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(); }}
- java的多线程同步
- Java 多线程的同步
- Java 多线程的同步
- Java多线程的同步
- Java多线程-线程的同步(同步方法)
- java多线程:线程的同步-同步块
- Java多线程-线程的同步(同步方法)
- Java多线程-线程的同步(同步方法)
- java的多线程同步初探
- Java多线程的同步机制
- java多线程的同步问题
- java的多线程同步初探
- Java多线程的同步问题
- java多线程之间的同步
- Java多线程的同步总结
- Java复习--多线程的同步
- Java多线程同步的方式
- Java多线程的同步机制
- ionic2环境配置
- bzoj 2161: 布娃娃 (扫描线+线段树)
- ubuntu 安装Android studio
- MySQL优化sql语句查询常用的30种方法
- myeclipse 2015EI快速整合三大框架
- Java多线程的同步
- java replace和replaceAll的区别以及用法
- OC runtime 类与对象
- 大数据教程(三) Hadoop集群分布式坏境搭建
- jmeter学习笔记(三)-性能测试概念
- 获取表单 input框中输入的值
- bootstrap里的弹框组件Model
- The tidy tools manifesto
- 解决“要登录到这台远程计算机,你必须被授予“的远程无法登陆问题