Java多线程的同步

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=200
sum=200
sum=400
sum=500
sum=300
sum=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=100
sum=200
sum=300
sum=400
sum=500
sum=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 free
Waiting 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();
//通过锁创建condition
condition 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);
    }
}
 
执行结果:
 wait
Interrupt come
wait 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.join()语句,所以只有主线程会等。

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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值