Java多线程的同步

来源:互联网 发布:淘宝抢购提醒查看 编辑:程序博客网 时间:2024/05/16 10:51

1.为什么要同步

2.同步关键字synchronized

3.死锁

4.等待/唤醒机制

5.锁对象---lock()接口

6.condition接口---条件对象

7.interrupt的使用

8.守护线程(后台线程)

9.join方法

10.yield方法


1.为什么要同步

在计算机中,多线程的实现是通过CPU轮流执行各个线程,当一个线程任务被多个线程执行时,就有可能造成线程安全问题,如下面的线程任务:

sum = 0;public void add(){     int n = 100;     sum = sum+n;     /*当线程1执行到这里时,sum=100,但还没来得及输出,CPU接切换到线程2去了,线程2的sum加了100后,sum=200,并输出了,就造成了200在100前输出,这样的结果就混乱了。所以要线程同步*/     System.out.println("sum="+sum);}

同步时,被同步的内容一次只允许有一个线程进入。


以银行存钱为例,给出未同步时的代码:

class Bank  //银行类{    private int sum=0;    public void add(int n)    {        sum += n;        System.out.println("sum="+sum);    }}class Customer implements Runnable  //储户方法类{    Bank b = new Bank();    public void run()       //每个储户存三次100    {        for(int i=0;i<3;i++)            b.add(100);    }}public class Main{    public static void main(String[] args) {        Customer c = new Customer();        Thread t1 = new Thread(c);  //创建两个线程加入到储户方法类中,相当于创建两个储户对象        Thread t2 = new Thread(c);        t1.start();        t2.start();    }}/*打印结果:sum=200sum=200sum=400sum=500sum=300sum=600*/

可以看出,打印结果是没有先后顺序的,且有重复。所以必须要对线程进行同步。


2.同步关键字synchronized

synchronized修饰需要被同步的代码

当两个线程Thread0 和 Thread1都执行到一个同步代码块时,Thread0和Thread1是互斥的,只有一个能执行同步代码块,另一个则阻塞。直接另一个线程执行完代码块。


注意:用多个线程操作同一个线程任务时,想要同步,必须操作的是同一个锁。

1.同步代码块

synchronized同步代码块使用实例:

synchronized(锁){}
实例:

(用上面的银行储户例子)

class Bank  //银行类{    private int sum=0;    private Object obj = new Object();  //创建一个对象相当于对象锁(this也算是一个对象锁,下面会讲到)    public void add(int n)    {        synchronized (obj) {        //给下面同步代码加锁            sum += n;            System.out.println("sum=" + sum);        }    }}class Customer implements Runnable  //储户方法类{    Bank b = new Bank();    public void run()       //每个储户存三次100    {        for (int i = 0; i < 3; i++) {            b.add(100);        }    }}public class Main{    public static void main(String[] args) {        Customer c = new Customer();        Thread t1 = new Thread(c);  //创建两个线程加入到储户方法类中,相当于创建两个储户对象        Thread t2 = new Thread(c);        t1.start();        t2.start();    }}/*打印结果:sum=100sum=200sum=300sum=400sum=500sum=600*/


2.同步函数

同步函数:在需要同步的函数名处用 synchronized 修饰就行了。

class Bank{private int sum =0;public synchronized void add(int n){sum+=n;System.out.println("sum="+sum);}}
对比同步代码块,同步代码块有个对象锁,那么同步函数就不需要锁了?

同步函数的锁用的是this。函数需要被对象调用,哪个对象不确定,但是都用 this 来表示。(不需要我们操作)

所以同步函数的锁一般都是 this


稍有不同的是 静态同步函数

因为静态的函数是不属于对象的,只属于类,所以锁就不能是 this 了。

类进内存的时候会生成一个关于该类的字节码对象。而静态同步函数的锁就是该字节码对象。 表示为:类名.class


同名代码块和同名函数的区别:

同步代码块可以使用任意对象所谓锁

同步函数只能使用 this 为锁


3.死锁

死锁的原因一般是在同步代码中嵌套了锁。

如有两个人,A拿着苹果,B拿着橙子。A说:你把橙子给我,我就给你苹果。 B说:你把苹果给我,我就给你橙子。  这样子两个人就陷入了僵持状态。

同样,对于两个线程也一样。

下面通过代码演示一下:

class MyLock    //锁是共用的{public static final Object LOCKA = new Object();//创建两个对象当作对象锁来用public static final Object LOCKB = new Object();}class Task implements Runnable{private boolean flag;Task(boolean flag){this.flag = flag;}public void run(){if(flag)//flag = true 的线程执行{synchronized(MyLock.LOCKA)//拿到LOCKA 等待LOCKB释放{System.out.println("Waiting LOCKB free");synchronized(MyLock.LOCKB){System.out.println("Get LOCKB");}}}else if(!flag)//flag = false的线程执行{synchronized(MyLock.LOCKB)//拿到LOCKA 等待LOCKB释放{System.out.println("Waiting LOCKA free");synchronized(MyLock.LOCKA){System.out.println("Get LOCKA");}}}}}class syn{public static void main(String []args){Task t1 = new Task(true);Task t2 = new Task(false);new Thread(t1).start();new Thread(t2).start();}}运行结果:Waiting for LOCKA freeWaiting for LOCKA free然后阻塞.....



4.等待/唤醒机制

介绍两个方法:  wait()和notify()。

1.这两个方法必须用在同步代码块或同步方法中。

2.使用声明锁。例如,同步代码块是用 Locka作为锁的,那么调用wait()或者notify()时,就要写成 Locka.wait() ,若没写,就相当于 this.wait()

3.调用wait()必须要在try-catch()语句中,或者声明一个throws异常,因为wait()会抛异常。

wait():该方法可以让线程处于冻结(阻塞)状态,并将线程临时存储到对应锁的线程池中。

notify():唤醒指定线程池中(用锁来指定)的任意一个线程

notifyAll():唤醒指定线程中的所有现场


有时候同步的一个标准是,两个进程,我走一步,你再走一步,不能我走两步你再走一步。要两个进程交替进行动作,就要用到等到./唤醒机制。

下面有一个例子:有一家商店,生成和消费一样商品,但该商店的规则是 生成一个商品就卖一个,再生产一个,即该商店顶多只有一个商品在卖,卖了再生生产。

class Store         //一个商店一边生产商品,一边卖出商品。{    private int ThingNum=0;         //商品数目    private boolean flag = true;    //标记    public synchronized void produce()  //生产商品方法    {        if(flag) {      //flag = true 就生产一个商品,否则就wait()阻塞。            ThingNum += 1;            notify();   //唤醒一个线程,因为这里只有两个线程(不算上main),所以唤醒的只有消费者线程了。            System.out.println("produce ----" + ThingNum);            flag = false;        }        else            try{wait();}catch(InterruptedException e){}     //wait()是使调用该方法的线程阻塞    }    public synchronized void consume()  //消费商品方法    {        if(!flag) {         //若flag = flase,消费商品            notify();       //唤醒一个进程,因为这里只有两个线程(不算上main),所以唤醒的就是生产者线程,告诉他可以生成商品了            ThingNum -= 1;            System.out.println("consume ----" + ThingNum);            flag = true;        }        else            try{wait();}catch(InterruptedException e){} //阻塞消费者进程    }}class Producer implements Runnable      //生产者线程任务{    private Store s;    //商店类的引用    Producer(Store s)   //传入商品对象    {        this.s = s; //使引用指向传入的对象    }    public void run()   //线程任务    {        while(true) {       //一直循环            s.produce();    //调用商店对象的生产方法        }    }}class Consumer implements Runnable      //消费者线程任务{    private Store s;    //商店类的引用    Consumer(Store s)   //传入商品对象    {        this.s = s; //使引用指向传入的对象    }    public void run()   //线程任务    {        while(true) {   //一直循环            s.consume();    //调用商店对象的生产方法        }    }}class Main{    public static void main(String []args)    {        Store s = new Store();            //创建一个商品类对象。接下来的线程都是对这个对象进程操作。        Producer p = new Producer(s);     //创建生产者线程任务对象        Consumer c = new Consumer(s);     //创建消费者线程任务对象        Thread t1 = new Thread(p);        //创建线程1---该线程任务是生产者任务        Thread t2 = new Thread(c);        //创建线程2---该线程任务是消费者任务        t1.start();                       //开启线程        t2.start();    }}

执行结果:

produce ----1
consume ----0
produce ----1
consume ----0
produce ----1
consume ----0
produce ----1
consume ----0
produce ----1


notifyAll()的使用:

上诉例子说的是一个线程生产,一个线程消费的情况。是较为片面的。下面演示用多个消费者线程和多个生产者线程。

就如:原本一间商店里,有一个师傅生产商品,有一个顾客来买商品。现在就变成有多个师傅生产商品,有多个顾客来买商品。同样规则是有一个就卖一个,还有商品就不继续生产,没了再生产。

思路:

notify()方法是随即唤醒一个线程池中的任意线程的,所以如果一有多个生产者线程或者多个消费者线程,就很容易出现问题,例如生产者唤醒线程时,唤醒的可能不是消费者线程,而是唤醒在阻塞的另一个生产者线程。所以为了确保生产者线程调用notify()时,一定会唤醒消费者线程,所以就要换成调用 notifyAll()把沉睡的线程全部唤醒,当然这样做也会把生产者其他沉睡的线程都唤醒,但可以让不像唤醒的生产者线程再次沉睡。

class Store         //一个商店一边生产商品,一边卖出商品。{    private int ThingNum=0;         //商品数目    private boolean flag = true;    //标记    public synchronized void produce()  //生产商品方法    {       while(!flag){         //循环判断flag的状态,如若生产者线程A唤醒了另一个生产者线程B,就让B通过判断再次沉睡           try{wait();}catch(InterruptedException e){}     //wait()是使调用该方法的线程阻塞       }        ThingNum += 1;        notifyAll();   //唤醒所有沉睡(阻塞)的线程        System.out.println(Thread.currentThread().getName()+":"+"produce ----" + ThingNum);        flag = false;    }    public synchronized void consume()  //消费商品方法    {        while(flag){        //循环判断flag的状态,如若消费者线程A唤醒了另一个消费者线程B,就让B通过判断再次沉睡            try{wait();}catch(InterruptedException e){}     //wait()是使调用该方法的线程阻塞        }        ThingNum -= 1;        notifyAll();   //唤醒所有沉睡(阻塞)的线程        System.out.println(Thread.currentThread().getName()+":"+"consumer ----" + ThingNum);        flag = true;    }}class Producer implements Runnable      //生产者线程任务{    private Store s;    //商店类的引用    Producer(Store s)   //传入商品对象    {        this.s = s; //使引用指向传入的对象    }    public void run()   //线程任务    {        while(true) {       //一直循环            s.produce();    //调用商店对象的生产方法        }    }}class Consumer implements Runnable      //消费者线程任务{    private Store s;    //商店类的引用    Consumer(Store s)   //传入商品对象    {        this.s = s; //使引用指向传入的对象    }    public void run()   //线程任务    {        while(true) {   //一直循环            s.consume();    //调用商店对象的生产方法        }    }}class Main{    public static void main(String []args)    {        Store s = new Store();            //创建一个商品类对象。接下来的线程都是对这个对象进程操作。        Producer p = new Producer(s);     //创建生产者线程任务对象        Consumer c = new Consumer(s);     //创建消费者线程任务对象        Thread t1 = new Thread(p);        //分别创建2个生产者,消费者线程        Thread t2 = new Thread(p);        Thread t3 = new Thread(c);        Thread t4 = new Thread(c);        t1.start();                       //开启线程        t2.start();        t3.start();        t4.start();    }}执行结果:        Thread-0:produce ----1        Thread-3:consumer ----0        Thread-1:produce ----1        Thread-2:consumer ----0



5.锁对象---lock()接口

lock接口是JDK 1.5版本才有的,而上面的同步synchronized 是JDK 1.4的产物。

lock()接口是用来替代同步的(synchronized),lock 对比 synchronized:

使用 Lock接口必须 import java.util.concurrent.locks.*;


1.lock()是个接口,要使用他就用先实现他,不过官方的文档也给出了已经实现的子类。lock()接口提供的子类有互斥锁,读锁,写锁。可重入锁是(ReentrantLock),可重入锁可被多次请求而不死锁。

2.synchronized 的锁是任意的,不确定的对象。而lock接口则把锁封装成一个对象 :lock()表示获得锁,unlock()表示释放锁。

3.synchronized 是自动上锁,自动释放锁的。而使用lock()接口,无论上锁或者是解锁都是要手动的。

4.用lock()接口最好用在 try-finally语句中,因为一个线程若在一个代码块中在上锁期间崩溃了,那么就解不了锁了。其他线程也进不了该代码块了

//解决方法try{     上锁;     对应操作;}finally{     释放锁}

下面给出演示代码: 用3个线程来打印0~100

import java.util.concurrent.locks.*;    //引入锁所在的包class Num       //数字类{    public int count = 0;   //计算的起始值为0    private Lock lock = new ReentrantLock();    //创建一个可重入锁    public void count_fun()         //数字类提供计数方法    {        lock.lock();        //上锁        try {            System.out.println(Thread.currentThread().getName() + ":" + count); //打印出 哪个线程打印哪个数字            count += 1;        }        finally {            lock.unlock();      //解锁        }    }}class Count implements Runnable     //计数的线程任务{    private Num n;    Count(Num n){        this.n = n;    };    public void run()    {        while(n.count<100)            n.count_fun();    }}class Main{    public static void main(String []args)    {        Num n = new Num();            //实例化数字类        Count c = new Count(n);       //实例化线程任务        Thread t1 = new Thread(c);            Thread t2 = new Thread(c);        Thread t3 = new Thread(c);        t1.start();        t2.start();        t3.start();    }}/** 打印结果:不同的线程合作打印100个数字* */



6.条件对象---condition接口

condition 同样也是JDK 1.5版本的产物,用来对应 Lock接口的。相当于 wait(),notify(),notifyAll() 对应 同步synchronized。

condition接口也包括 wait(),notify() ,notifyAll() 对应功能的方法。他们分别是 await(),signal(), signalAll()

await()和wait()都是被唤醒后,就在刚刚阻塞的地方继续执行下去。

1.Lock锁对象保证了每次只有一个线程能够进入临界区

2.但线程进入临街区后,发现要在某一条件满足之后它才能执行,这时候线程就应该阻塞在那里,等待条件满足后才继续执行。

3.但线程在临界区不动又不做其他事情的话,意味着锁还被它占着,那么其他线程也不能进入临界区,就好像占着茅坑不拉屎。

4.这时候就用到了条件对象condition中的await()方法,使线程先冻结,并释放锁。等待其他线程让条件满足后,其他线程再调用condition的sigal()来唤醒被冻结的线程

5.被冻结的线程被唤醒后,马上又加上锁,并继续执行下去。


下面展示一下条件对象被创建的代码。再演示一下使用条件变量的例子:

//创建锁Lock lock = new ReentrantLock();//通过锁创建conditioncondition con = lock.newCondition();

condition 接口中有 await(),signal(),singalAll()

//con 是condition接口的一个实现类的对象con.await();//调用await方法con.signal();//调用signal方法con.sigalAll();//调用signalAll()方法


演示条件变量的例子:若一个人进茅坑,把厕所锁上(lock上锁),然后发现茅坑爆了,只好什么都不做,并解开厕所的锁,在厕所外等着,等着别人把茅坑修好,再锁上门,再继续使用厕所。而且一个人用完一次,茅坑就爆一次,必须要人来修。

1.一个人相当于一个线程

2.把厕所锁上,相当于上锁

3.厕纸相当于条件对象

import java.util.concurrent.locks.*;    //引入锁所在的包class Toilet        // 厕所类,提供茅坑的状态,用茅坑的方法,修茅坑的方法,茅坑的锁{    private boolean MaoKeng = false;    private Lock lock = new ReentrantLock();    private Condition MaoKeng_Condition = lock.newCondition();        public void fix_MaoKeng()   //修茅坑的方法    {        lock.lock();    //看到茅坑必须先看有没有锁门,要是有锁门就在门外等着锁解开        try {            while (MaoKeng) {       //当茅坑的状态为true(即好)的时候                try {                    MaoKeng_Condition.await();      //解开茅坑的锁,等到茅坑坏了的时候再来                } catch (InterruptedException e) {                }            }                        MaoKeng = true;      //修好后,茅坑的状态变为好的            System.out.println(Thread.currentThread().getName()+"茅坑修好啦,可以来人用了");            MaoKeng_Condition.signalAll();      //此时可以叫别人来用了(唤醒await()的线程)        }        finally {            lock.unlock();        }    }    public void use_MaoKeng()   //用茅坑的方法    {        lock.lock();    //看到茅坑必须先看有没有锁门,要是有锁门就在门外等着锁解开        try{            while(!MaoKeng) {   //若茅坑的状态为坏                try {                    MaoKeng_Condition.await();     //解开茅坑的锁,并在厕所外等着,直到修好后,被人叫去用                } catch (InterruptedException e) {                }            }            MaoKeng = false;    //用完茅坑后,茅坑坏了            System.out.println(Thread.currentThread().getName()+"茅坑坏啦,来人修啊");            MaoKeng_Condition.signalAll();  //叫人来        }        finally {            lock.unlock();        }    }}class Fix_worker implements Runnable    //修理工类{    private Toilet t = new Toilet();    Fix_worker(Toilet t)    {        this.t = t;    }    public void run()    {        for(int i=0;i<10;i++) {     //例 修理工每天要修10次茅坑才能下班            t.fix_MaoKeng();        }    }}class User implements Runnable      //使用者类{    private Toilet t = new Toilet();    User(Toilet t)    {        this.t = t;    }    public void run()    {        for(int i=0;i<10;i++) {     //例使用者每天要使用10次茅坑            t.use_MaoKeng();        }    }}class Main{    public static void main(String[] args)    {        Toilet t = new Toilet();        Fix_worker w = new Fix_worker(t);        User u = new User(t);        Thread t1 = new Thread(w);          Thread t2 = new Thread(u);        Thread t3 = new Thread(u);        Thread t4 = new Thread(u);        t1.start();        t2.start();        t3.start();        t4.start();    }}打印结果:        Thread-0茅坑修好啦,可以来人用了        Thread-1茅坑坏啦,来人修啊        Thread-0茅坑修好啦,可以来人用了        Thread-2茅坑坏啦,来人修啊        Thread-0茅坑修好啦,可以来人用了
从运行结果看出:茅坑坏了才 有人来修,修了才有人来用,不会没修好又有人来用。这就是线程的同步。



7.interrupt的使用

interrupt()的作用主要是提前结束线程因调用 Object.wait(),Condition.await(),Thread.sleep(),Thread.join()而引发的阻塞,并抛出一个InterruptException异常,若线程在正常运行,则调用interrupt() 不会有反应。

class Th implements Runnable{    public synchronized void fun()    {        try{            System.out.println("wait");            this.wait();        }        catch (InterruptedException e){            System.out.println("Interrupt come");        }        System.out.println("wait over");    }   public void run()   {       fun();   }}class Main{    public static void main(String []args)throws Exception    {        Th th = new Th();        Thread t = new Thread(th);        t.start();        t.interrupt();        Thread.sleep(1000);    }}执行结果: waitInterrupt comewait over

8.守护线程(后台线程)

守护线程也称为后台线程,他的特点是:如果当前台线程(非后台线程)都结束了,那么后台线程也会自动结束。后台线程就像是为前台线程提供服务的,若是前台线程都结束了,那么后台线程当然也要结束了。

将某一线程标记为后台线程:

Thread t = new Thread(x);//创建一线程t.setDaemon(true);//将线程t设置为后台线程

9.join方法

join方法会让调用join方法的线程得到执行权

例如:

class Main//主线程{    public static void main(String[] args)throws InterruptedException    {        T0 t0 = new T0();        T1 t1 = new T1();        Thread tt0 = new Thread(t0);        Thread tt1 = new Thread(t1);        tt0.start();                tt0.join();         //主线会阻塞在这里,等待tt0线程结束后,主线程才会继续执行        tt1.start();    }}
当主线程阻塞的时候,其他线程还是会照执行不误。因为只有主线程中有 tt0.joi()语句,所以只有主线程会等。

10.yield方法

yield()方法可以让暂停当前正在执行的线程对象,并切换去执行其他线程。

若有两个线程t0,t1。 两个线程在打印一句话后,都会调用 yield()方法。

这样的执行结果是两个线程打印的次序很大程序上会交叉打印,但并不能要求一定会交叉打印,因为当一个线程调用yield方法后,有可能他自己又抢到CPU资源。

class T0 implements Runnable{    public void run()    {        for(int i=0;i<5;i++)        {            System.out.println(Thread.currentThread().getName());            Thread.yield();        }    }}class T1 implements Runnable{    public void run()    {        for(int i=0;i<5;i++)        {            System.out.println(Thread.currentThread().getName());            Thread.yield();        }    }}class Main{    public static void main(String[] args)throws InterruptedException    {        T0 t0 = new T0();        T1 t1 = new T1();        Thread tt0 = new Thread(t0);        Thread tt1 = new Thread(t1);        tt0.start();        tt1.start();    }}


0 0
原创粉丝点击