赞
踩
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();
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。