Java并发编程基础(入门篇)

时间:2020-06-18 13:02:57   收藏:0   阅读:49

@

java中的多线程(入门)

1.线程

1.1多线程的执行原理

先以一个Java中多线程的Demo为例展开解释,请看代码:

//自定义的多线程类
public class MyThread extends Thread{
    public MyThread(String name){
        super(name);
    }
    //重写run方法
    @Override
    public void run(){
        for(int i=0;i<20;i++){
            System.out.println(getName()+"_"+i);
        }
    }
}
//测试类
public class ThreadTest {
    public static void main(String[] args) {
        //多线程的执行原理及其步骤:
        //1.Jvm执行main方法,找OS开辟一条通向cpu的路径,这个
        //路径就叫mian线程(主线程),cpu通过这个路径执行main中的方法
        //新开辟一条通向cpu的新路径,这条新路径就是MyThread线程
        MyThread mt=new MyThread("bob");
        //使用MyThread线程执行run方法
        mt.start();
        for(int i=0;i<20;i++){
            System.out.println("mian_"+i);
        }
    }
}

上述代码执行流程:

1.2多线程的创建方法

方法1: 继承Thread类,在子类中重写run()方法,start()方法开启新的线程;

public class MyThread extends Thread{

    public MyThread(String name){
        super(name);
    }

    @Override
    public void run(){
        for(int i=0;i<20;i++){
            System.out.println(getName()+"_"+i);
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
         MyThread mt=new MyThread("bob");
         mt.start();
    }
}

方法2: 实现Runnnable接口,将该子类作为参数传递给Thread的构造方法;使用Thread对象的start()方法启动新的线程;

public class MyRunnable implements Runnable{
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"_"+i);
        }
    }
    
}
public class ThreadTest {
    public static void main(String[] args) {
        Thread th=new Thread(new MyRunnable());
        th.start();
        System.out.println("主线程");
    }
}

1.3Thread和Runnable的区别

实现Runnable的好处:

1.4匿名内部类方式实现线程的创建

public class ThreadTest {
    public static void main(String[] args) {
        //匿名内部类
        //规则:new 父类/接口(){
            //重写父类中的run方法;
        //}
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName());
                }
            }
        }).start();
        System.out.println("main");
    }
}

2.线程的安全

多线程访问了共享数据会导致线程问题
卖票案例

public class Ticket implements Runnable {
    private int tickets=100;
    @Override
    public void run(){
        while(true){
            if(tickets>0){
                try{
                    Thread.sleep(100);
                }catch(Exception e){
                    e.printStackTrace();
                }
                String name=Thread.currentThread().getName()+"正在卖票";
            System.out.println(name+"正在卖票:"+tickets--);
            }
        }
    }   
}
public class SaleTickets {
    public static void main(String[] args) {
        Ticket ticket=new Ticket();
        Thread t1=new Thread(ticket,"窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

产生安全问题的原因

多个线程访问了同一个变量。

使用java的线程同步机制来解决线程安全问题

//同步块
public class Ticket implements Runnable {
    private int tickets=100;
    Object obj=new Object();
    @Override
    public void run(){
        while(true){
            if(tickets>0){
                synchronized(obj){
                    try{
                        Thread.sleep(100);
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                    String name = Thread.currentThread().getName() + "正在卖票";
                    System.out.println(name + "正在卖票:" + tickets--);
                }
            }   
        }
    }   
}
//同步方法块
public class Ticket implements Runnable {
    private int tickets=100;
    Object obj=new Object();
    @Override
    public void run(){
       saleTicket();
    }
    public synchronized void saleTicket(){
        while (true) {
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName() + "正在卖票";
                System.out.println(name + "正在卖票:" + tickets--);
            }
        }
    }
}
//使用lock锁机制
public class Ticket implements Runnable {
    private int tickets=100;
    //创建锁对象
    Lock lock=new ReentrantLock();
    @Override
    public void run(){
        while (true) {
            lock.lock();//加锁
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName() + "正在卖票";
                System.out.println(name + "正在卖票:" + tickets--);
            }
            lock.unlock();//释放锁
        }
    }
}

同步原理

t0、t1、t2三个线程同时去抢夺cpu资源,假如t0获取到了cpu执行权限,t0遇到synchronized(object),获取对象锁,进入到同步代码块,当同步代码块执行完成之后,才归该对象锁。

3.线程的状态

生产者消费者案例

public class WaitAndNotify {
    public static void main(String[] args) {
        //保证生产者和消费者使用同一个对象锁
        Object obj=new Object();
        //消费者
        new Thread(new Runnable(){
            @Override
            public void run() {
                while(true){
                    synchronized (obj) {
                        System.out.println("我要吃包子");
                        try {
                            obj.wait();// 当前线程进入永久等待,并且规划对象锁,直到收到唤醒通知;
                            System.out.println("开始吃包子");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
        //生产者
        new Thread(new Runnable(){
            @Override
            public void run() {
                while(true){
                    // 唤醒消费者线程
                    synchronized (obj) {
                        try {
                        // 生产包子,1秒
                        Thread.sleep(1000);
                        } catch (Exception e) {
                        e.printStackTrace();
                    }
                        obj.notify();//发出唤醒由该对象锁进入等待的线程,代码出了synchronized代码块后自动归还对象锁;
                    }
                }  
            }
        }).start();
    }
    
}

使用了object.wait()之后,当前线程处于永久等待,并且归还该对象锁,直到收到object.notify()的唤醒通知。


并发编程

并发编程的硬件基础

现代CPU架构中,一个CPU通常可以包含多个物理内核,而一个内核又通过虚拟技术可以实现双线程,因此一个两核CPU可以同时运行4个线程。

什么是并发?

在指定的一段时间内,可同时运行多个线程,并发编程的目的在于确定每个线程任务分配的cpu及其相应的开始和结束时间以使得cpu的利用率最高,同时必须保证线程的安全!

并发编程的三大特性

原子性
即定义的一系列操作要么全部执行成功,要么一个都不执行。
可见性
多个线程共享同一个变量的时候,当一个线程修改了该变量的时候,其他线程必须能够立刻获取到修改后的值。
有序性
程序执行的顺序按照代码的先后顺序执行;java编译器为了提高程序效率,会对代码的指令进行重排,在单线程程序中,会保证程序最后的执行结果和重排序之前一致,但是在多线程程序中,有可能会影响并发的结果。因此,在并发编程中,必须保证有序性


java如何保证并发编程的三大特性

Java的内存模型

Java内存模型规定,所有的变量均存储在主存(物理内存),每一个线程都拥有自己的工作内存,也叫缓存(主存中的副本,线程不直接修改主存中的变量,每个线程不能访问其他线程的工作内存)cpu中还有高速缓存行(l1,l2,l3寄存器),线程的工作内存数据先被读取到cpu缓存器中,因此数据存在三个级别的。举个例子:

 i=250;

执行该行代码的时候,先从主存中取i的值到当前线程的工作内存中,然后将250赋值给工作内存中的i变量,最后,将工作内存中的i变量写入到主存中去。


原子性
java中对单个的读或者单个的写操作,是具有原子性的,即要么成功,要么失败。但是对任何单一的操作的组合而成的复杂操作是无法保证原子性的。举个例子:

i++;

i++自增操作不具备原子性的,为什么?i++操作其实是由三个操作构成:1)读取i的值。2)i的值增1. 3)将新值赋值给i变量;java语言只能保证单一的操作具有原子性。而要保证诸如i++等的原子性,可以使用synchronizedlock锁机来同步这些代码块,可以保证并发编程的原子性。

可见性
那java中如何保证变量的可见性呢?volatile关键字。当被volatile修饰的变量(当前线程的共内存中)被修改时,能被立即刷新到主存中去。因此,可以保证在其他线程还没有从主存中读去新值的情况下,可以得到最新的值。 ,同时 synchronized 和 lock能保证在同一个时刻只有一个线程能获取到对象锁(对象监视器)然后执行同步代码块,并且在释放对象锁之前将修改的值刷新到主存中去,从而在一定程度上保证了变量的可见性。

有序性
Java内存模型中,允许编译器和处理器对指令进行重排,指令重排不会影响共到单线程,但是会影响到多线程并发执行的正确性。java中的"happens-before"原则能保证一定的有序性。"happen-before"有8大原则:

volatile关键字的详细解释

一个共享变量如果被volatile所修饰,那么该变量就具备以下两种含义:

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;

分析: 线程1中执行以下操作,先将主存中的stop变量读取到线程1的工作内存中,线程1进入死循环,一直执行doSomething()操作。而当线程2开始执行的时候,情况变了,线程2先将主存中的stop变量读取到工作内存中,此时有可能存在两种情况:1)线程2修改了工作内存中的stop变量,但是没有写入到主存中去就被终止了。 2)线程2修改了工作内存中的stop变量,并写入到主存中去。当情况1)发生时,那么线程1进入死循环。当情况2)发生时,线程2可以退出死循环。那么如何才能保证上述代码只正确执行呢??使用volatile修饰stop保证这个变量的可见性,即当这个变量在某个线程的工作内存中修改是就立即被刷新到主存中,其他线程工作内存中的缓存状态失效,并重新从主存中读取新值。


volatile能保证原子性吗
先上个案例,

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1) {
            //保证前面的线程都执行完
            Thread.yield();
        } 
        System.out.println(test.inc);
    }
}

分析: 代码创建了10个线程,每个线程使得inc自增1000,同时使用volatile修饰了inc变量,那inc最终的结果是10*1000吗??答:不是。为什么呢?因为虽然inc具有可见性,但是inc++操作不具备原子性,我们可以设想到这样的一个场景,某个状态下,inc=10,线程1从主存中读取了inc的值到工作内存中,inc还未进行自增的时候,线程1的cpu执行权限到了,转而去执行线程2,此时,线程2从主存中将inc读取到工作内存中,再将Inc读取到cpu的高速缓存寄存器中,进行自增操作inc+1=11,并写回主存,线程1已经读取了inc到工作内存中并且读取到cpu的l1、l2中去了,即使缓存中的数据更新过来,cpu用的仍然是更新之前的数据,因此也不会重新读取缓存中的值随后刷新到主存中,因此两个线程inc增加了1。

改进
使用synchronized或则lock机制来保证inc操作具有原子性。就可以让线程1准确地保证可见性,从使得并发能正确进行

public class Test {
    public volatile int inc = 0;
    Lock lock = new ReentrantLock();
     //使用syunchronized保证原子性
    public synchronized void increase() {
        inc++;
    }
    //使用lock同步代码块,保证原子性
    public void increase2(){
        try{
            lock.lock();
            inc++;
            lock.unlock();
        }catch(Exception e){
            ..
        }
    }
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1) {
            //保证前面的线程都执行完
            Thread.yield();
        } 
        System.out.println(test.inc);
    }
}

结论: volatile 能保证共享变量的可见性,但是无法保证对该变量的操作的原子性,保证原子性可以通过synchronized或者lock来保证。

volatile的作用


线程内的共享变量(ThreadLocal)

ThreadLocal对象用于某个线程内的共享变量,ThreadLocal对象提供两个方法:set()和get()方法,两个方法实现底层为一个Map,Map的键为当前线程的ThreadLocal对象。因此不同的线程使用get()能拿到属于自己线程内的ThreadLocal对象,也就能保证拿到其中的值;同时,在同一个线程中能保证一个ThreadLocal对象只存储一个值,实现了线程内的共享。看个例子吧:

//Runnable的子类
public class MyThreadLocal implements Runnable {
    //private Student student=new Student();
    ThreadLocal<Student> tl=new ThreadLocal<Student>();
    @Override
    public void run(){
        String currentThread=Thread.currentThread().getName();
        System.out.println(currentThread+"正在运行");
        System.out.println("开始设置对象的属性");
        Random random=new Random();
        Student student = getStudent();
        student.setAge(random.nextInt(100));
        System.out.println(currentThread+"线程中,第一次的年龄为:"+student.getAge()+",哈希值为:"+student.hashCode());
        try {
            Thread.sleep(1000);//让其他线程运行;
        } catch (Exception e) {
            e.printStackTrace();
        }
        student.setAge(random.nextInt(100));
        System.out.println(currentThread + "线程中,第二次的年龄为:" + student.getAge() + ",哈希值为:" + student.hashCode());
    }
    public Student getStudent(){
        Student s=tl.get();
        if(null==s){
            s=new Student();
            tl.set(s);
        }
        return s;
    }
}
//测试类
public class testThreadLocal {
   public static void main(String[] args) {
        MyThreadLocal threadLocal=new MyThreadLocal();
        new Thread(threadLocal,"线程1").start();
        new Thread(threadLocal,"线程2").start();
   }
}

分析:在创建要线程内共享的变量之前,先从ThreadLocal中get()一份,如果有就不创建,没有才创建,并且使用set()方法将刚刚创建的对象存到ThreadLocalMap中去。


多个线程之间的数据共享

java传统的对线程间的数据共享通常有两种形式:

public class testShareData {    
    public static void main(String[] args) {
         ShareData shareData=new ShareData(10);
         for(int i=0;i<4;i++){
             if(i%2==0){
                 new Thread(new IncRunnable(shareData), "Thread_" + i).start();
             }else{
                 new Thread(new decRunnable(shareData), "Thread_" + i).start();
             }
             
         }
    }   
}

// 共享数据的自增行为的Runnable类
class IncRunnable implements Runnable {
    private ShareData shareData;

    public IncRunnable(ShareData shareData) {
        this.shareData = shareData;
    }

    private void incShareData() {
        shareData.inc();
    }

    @Override
    public void run() {
        incShareData();
    }
}

// 共享数据的自减行为的Runnable类
class decRunnable implements Runnable {

    private ShareData shareData;

    public decRunnable(ShareData shareData) {
        this.shareData = shareData;
    }

    private void decShareData() {
        shareData.dec();
    }

    @Override
    public void run() {
        decShareData();
    }
}
public class ShareData {
    // 共享数据封装类
    private int num;
    public ShareData(int num) {
        this.num = num;
    }
    // 数据自增
    public synchronized void inc() {
        num++;
        System.out.println(Thread.currentThread().getName()+"num值为:"+num);
    }
    // 数据自减
    public synchronized void dec() {
        num--;
        System.out.println(Thread.currentThread().getName() + "num值为:" + num);
    }
}

方法2 使用匿名内部类的方式创建Runnable对象,可以减少参数传递的步骤。

public class testShareData {    
    public static void main(String[] args) {
         //确保shareData这个引用只会指向一个new ShareData()对象
         final ShareData shareData=new ShareData(10);
         for(int i=0;i<4;i++){
             if(i%2==0){
                 new Thread(new IncRunnable(){
                     @Override
                     public void run(){
                        shareData.inc();
                     }
                 }, "Thread_" + i).start();
             }else{
                 new Thread(new decRunnable(){
                    @Override
                     public void run(){
                        shareData.dec();
                     }
                 }, "Thread_" + i).start();
             }
             
         }
    }   
}
public class ShareData {
    // 共享数据封装类
    private int num;
    public ShareData(int num) {
        this.num = num;
    }
    // 数据自增
    public synchronized void inc() {
        num++;
        System.out.println(Thread.currentThread().getName()+"num值为:"+num);
    }
    // 数据自减
    public synchronized void dec() {
        num--;
        System.out.println(Thread.currentThread().getName() + "num值为:" + num);
    }
}

总结面向对像的特性之一:封装;

原文:https://www.cnblogs.com/Thinker-Bob/p/13156714.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!