多线程学下来之后,感觉还是挺晕的,但是还好,能听的懂。。。
不过重要的还是要做一下学习笔记,那接下来我们又学习了多线程里面更加复杂深层次的一些东西,经过一番学习,对多线程里面的部分知识点我都给他叫上一个小名:
线程间通信等待唤醒机制-->小时候玩的游戏 ”冰雪融化”。
同步锁-->”随用随借,用完就还”
死锁-->”进不来出不去”
1. 同步锁的出现:
什么叫同步锁?
锁定是控制多个线程对共享资源进行访问的工具。通常,锁定提供了对共享资源的独占访问。一次只能有一个线程获得锁定,对共享资源的所有访问都需要首先获得锁定。
前面我们学过因为多线程的存在,这样难免存在一个共有资源被多个线程抢占使用的情形,这就导致了共享数据正确同步的问题,比如说我们多个窗口卖票,我们怎么可能接受买到票号为0的票,或者票号为负数的票,这里面就存在一个多线程资源抢占带来一个数据同步和正确性的问题,这时候才出现一个锁,这个锁可以保证在某一段时间内只有一个线程在使用这个共有资源,待这个线程使用完这个资源之后将锁归还,然后和之前其他被锁阻塞在外的线程再次加入抢锁,使用资源的队列中,那么java中的这个锁叫做同步锁,用synchronized关键字了修饰定义。
然后我们来写一段代码定义几个线程并来使用同步代码块锁:
/* 下面这段代码使用来实现两个窗口同时共售票100张票,也就说卖一张少一张,而且不能出现100之外的其他任何票号 */ packagecom.threadDemo; public classSaleTicketDemo { public static voidmain(String[] args) { //创建两个线程并启动,两个线程操作的都是一个对象的run()方法。 SaleTicket st = newSaleTicket(); Threadt1 = newThread(st); Threadt2 = newThread(st); t1.start(); t2.start(); } } class SaleTicket implements Runnable { //声明总票数 private int ticket = 100; //用来作为同步块的锁作为同步块的参数传递,同步块的锁其实可以任意传//入一个对象即可,能当作唯一性的锁就可以了,这个我也不知道为什么。 Object obj = new Object(); @Override public void run() { while(true) { /* 在While循环内加入sychronized块,并将涉及到操作共有数据ticket变量的代码加入到同步块中,还要将o这个对象作为锁作为参数传递给同步块。所以当一个线程执行到这后,拿到锁,进入之后,就自动加锁了,其他的线程无法进入该同步块,因为它没锁,待线程执行完后,将锁归还,其他的线程//才有机会抢到锁进入执行同步块中的语句了,那么这里锁就是这个o对象。 */ synchronized(obj) { if(ticket >0) { try{ Thread.sleep(10);}catch(Exception e){}; System.out.println(Thread.currentThread().getName()+"<<>>>"+ticket--); } } } } }
执行结果如下:
Thread-0<<>>>100
Thread-0<<>>>99
Thread-0<<>>>98
……
Thread-1<<>>>96
Thread-1<<>>>95
Thread-1<<>>>94
……
Thread-1<<>>>72
Thread-0<<>>>71
Thread-0<<>>>70
……
Thread-0<<>>>63
……
Thread-0<<>>>1
所以我们发现程序正确的完成了我们想要的功能,没有出现0号票和负数票,而且是交替的在执行。
假设以上同步块拿掉之后,执行结果会是怎么样呢?发现这个同步块拿掉之后,打印的结果是两个线程在交替执行,但是却出现了0号票。
为什么会出现零号票呢?我们分析是不是会出现这样一种情况呢,假设现在ticket值为1,0线程先进来,进来之后执行了sleep,然后1线程又进来了,此时0线程sleep时间已过,执行了ticket--,然后线程1也醒过来了,执行了ticket--,那么这时候是不是打印了0号票呢?所以没有同步锁的存在就发生了线程安全性问题。
2. 静态同步函数的锁和非静态同步函数的锁
静态同步函数:简单理解就是在一个类中静态方法上使用synchronized修饰的方法,非静态就不多说了,当然是修饰在非静态的实例方法上面。
我们来通过一段代码来区分非静态函数和静态函数的区别,以及它们在线程执行到该同步函数的时候,使用的锁到底是哪一个。
packagecom.threadDemo; public classSaleTicketDemo { public static voidmain(String[] args) { SaleTicketst = newSaleTicket(); Threadt1 = newThread(st); Threadt2 = newThread(st); t1.start(); try{ Thread.sleep(10);}catch(Exception e){}; st.flag = false; t2.start(); } } class SaleTicket implements Runnable { private int ticket =100; boolean flag = true; @Override public void run() { if(flag) { while(true) { //这是一个普通的同步块,使用锁是this,this关键之只能在非静态的方法中使用 //原因待会再讲 synchronized(this) { if(ticket >0) { try{ Thread.sleep(10);}catch(Exceptione){}; System.out.println(Thread.currentThread().getName()+"<<>>>"+ticket--); } } } } else { while(true) { show(); } } } //在非静态方法上使用synchronized修饰,this表示当前对象,创建了一个非静态 //同步函数锁 public synchronized void show() { if(ticket >0) { try{ Thread.sleep(10);}catch(Exception e){}; System.out.println(Thread.currentThread().getName()+"[[[[]]]]"+ticket--); } } }
执行结果:
Thread-0<<>>>100
Thread-0<<>>>99
Thread-0<<>>>98
Thread-0<<>>>97
……
Thread-0<<>>>85
Thread-0<<>>>84
……
Thread-1[[[[]]]]83
Thread-1[[[[]]]]1
所以从以上的执行结果可以看的出,结果中没有出现不理想的票号,这就说明非静态同步函数和同步块使用的是一个锁,因只有一个锁才能保证共享数据的同步,也就是说只有一个锁,某一段时间只有持锁的线程在操作数据,操作完之后其他的线程才有机会拿到锁去执行,大家看得到我在同步块中传入的锁是this,所以得知非静态同步函数中使用的锁也是this。
在来看一段代码,我们在看看静态代码块又是怎么一回事呢?
packagecom.threadDemo; public classSaleTicketDemo { public static voidmain(String[] args) { SaleTicketst = newSaleTicket(); Threadt1 = newThread(st); Threadt2 = newThread(st); t1.start(); try{ Thread.sleep(10);}catch(Exception e){}; st.flag = false; t2.start(); } } class SaleTicket implements Runnable { //要在静态方法中使用成员变量,就要将ticket使用static修饰 private static int ticket = 100; boolean flag = true; @Override public void run() { if(flag) { while(true) { //这里还是传入this锁 synchronized(this) { if(ticket > 0) { try{ Thread.sleep(10);}catch(Exception e){}; System.out.println(Thread.currentThread().getName()+"<<>>>"+ticket--); } } } } else { while(true) { show(); } } } //这个方法改为了static修饰,并且加上synchronized,这样就创建一个//静态同步锁。 public static synchronized void show() { if(ticket > 0) { try{ Thread.sleep(10);}catch(Exception e){}; System.out.println(Thread.currentThread().getName()+"[[[[]]]]"+ticket--); } } }
执行结果:
Thread-0<<>>>100
Thread-1[[[[]]]]99
Thread-0<<>>>98
Thread-1[[[[]]]]97
Thread-0<<>>>96
Thread-1[[[[]]]]95
Thread-0<<>>>94
……
Thread-0<<>>>1
Thread-1[[[[]]]]0
所以从结果看得出,结果发生了变化,不良的票号终于出现了,0票号来了
,所以肯定是出现两个锁,导致两个线程都使用各自的锁,最终发生0票号的情况,而我同步块中使用的是this锁,那既然使用的是不同的锁,那么静态同步函数中使用的锁是哪一个呢?由此我们可以联想到在静态的方法中不能引用this,因为静态方法在被加载的时候也被存放到了内存中,而此时没有对象生成,所以this是不能使用的,那么此时存在的只有类的class文件或者说类的字节码文件对象,所以静态同步函数使用的锁是类的字节码文件对象,是类.class。
3. 线程中引发的单利设计模式(懒汉式和饿汉式)
饿汉式:
public class Singler { //最好加上final使其指向的对象终生不变 privatestatic final Singler s = new Singler(); privateSingler(){}; publicstatic Singler getInstance() { return s; } }
public class Singler { //这里千万不能加final,否s的值则始终是null了 private static Singlers = null ; private Singler() { } public staticSingler getInstance() { If(s == null) { //倘若这段代码在被多线程使用,那么当某个线程执行到if语句内//的话,就被CPU调度或者进入sleep等情况,其他线程进入if语句//内new出了一个Singler对象并返回给方法调用出,那么等最开 //的那个线程切换回来的话,又new出来一个新的Singler对象了,//那么同步锁就能解决这样的问题。 s = new Singler(); return s; } return s; } }
懒汉式同步版本:
解决了以上懒汉式在多线程同时访问static共享资源时对象延迟加载的弊端,使用同步锁可以解决这个问题,但是加上同步锁之后就稍微影响了程序的效率,那么可以加入双重判断,来加以缓和。
public class Singler { private staticSingler s = null ; private Singler() { } public staticSingler getInstance() { if(s == null) { synchronized(Singler.class) { If(s == null) { s = new Singler(); return s; } } } return s; } }
4. 死锁
死锁一般出现在两个嵌套同步函数或同步块中,比如说A线程拿着Lock1的锁,然后却没有内层同步块的锁Lock2,而另一个线程B拿着Lock2的锁,却没有内层同步块的锁Lock1,这样就导致的死锁,导致程序暂停等现象。
直接看代码:
package com.thread; public class DeadLockDemo { public static voidmain(String[] args) { Thread t1 = new Thread(new DeadLock(true)); Thread t2 = new Thread(new DeadLock(false)); t1.start(); t2.start(); } } class DeadLock implements Runnable { private boolean flag = false; public DeadLock( boolean flag) { this.flag = flag; } @Override public void run() { if(flag) { while(true) { //在某一时刻某个线程拿到obj1这个锁,想要进入之后想获得obj2的锁,却发现//这个锁被另一个线程在使用,即else语句块中,而此时没执行完外层的同步块 //内语句,而里层同步块语句因为没obj2这个锁,导致它此时"进不来出不去" //的局面,进入阻塞状态等待obj2钥匙被释放。 synchronized(Lock.obj1) { System.out.println("if synchronizedlock.obj1"); synchronized(Lock.obj2) { System.out.println("ifsynchronized lock.obj2"); } } } } else { while(true) { // 在某一时刻某个线程拿到obj2这个锁,想要进入之后想获得obj1的锁,却发//现这个锁被另一个线程在使用,即if语句块中,而此时没执行完外层的同步块之内语句,而里 //层的同步块语句因为没obj1这个锁,导致它此时"进不来出不去" //的局面,进入阻塞状态等待obj1钥匙被释放。 synchronized(Lock.obj2) { System.out.println("elsesynchronized lock.obj2"); synchronized(Lock.obj1) { System.out.println("else synchronized lock.obj1"); } } } } } } //定义了两个锁 class Lock { public static Object obj1 = new Object(); public static Object obj2 = new Object(); }
执行结果:
else synchronized lock.obj2
if synchronized lock.obj1
以上就是一个典型死锁例子。
5. 线程间的通信的等待和唤醒机制
比如说我们要实现这样一个功能,有一个劳务所,一个公司人力资源部,一个公司的某个工厂,劳务所给公司人力资源部输送人力,然后人力资源部又将输入过来的人力派送到工厂上班,那么这个功能如果用多线程来实现,要 如何作呢?
首先从上面的这段内容可以抽出三个主要信息,一个是资源,那么这里指的是劳工,另一个是输送人力的情景,一个派发人力到工厂的情景。
package com.thread; public class WaitAndNotify { public static void main(String[] args) { Person p = new Person(); Input in = new Input(p); Output out = new Output(p); Thread t1 = new Thread(in); try{Thread.sleep(10);}catch(Exception e){}; Thread t2 = new Thread(out); t1.start(); t2.start(); } } /* * 定义一个人的资源,里面有人的姓名和性别,已经设置姓名和性别以及获得姓名和性别的方法。 */ class Person { private String name ; private String sex; //定义一个标记,用来标记劳务所不要重复输送一个相同的人,和人力资源部不//要重复的去派发同一个人到工厂上班,也就是说实现送一个拿一个交替效果 public boolean flag; public void setName(Stringname) { this.name = name ; } public void setSex( String sex) { this.sex = sex; } public StringgetName() { return this.name; } public String getSex() { return this.sex; } } /* * 接下来要定义两个线程,一个input 一个output的线程, * 表示培训机构向公司输送人力,公司又向集团内的工厂 * 派送人力。 */ class Input implements Runnable { //要对劳务,人这个资源进行操作,就要持有一个Person的引用。 private Person p; public Input(Person p) { this.p = p; } @Override public void run() { int x = 1; while(true) { synchronized(p) { if(p.flag) { //这里判断flag是否为true,假如为true表示里面输送过去了一个劳务,然后就执//行wait(),交出执行权,让执行权交给人力派发的一方去派发人到工厂。 try{p.wait();} catch(Exception e){}; } else { //这里面的方法主要就是简单的模拟不断的输送劳务,这里就用不断更换人名和//性别替代 if(x == 0) { p.setName("Ansen_zhang"); p.setSex("男"); } else { p.setName("Lily_wang"); p.setSex("女"); } //当flag不为true的时候,表明劳务已经被派发出去了,要重新输送人 //过来,当输送完之后,将标记设为flag,并通知唤醒派发人员去派发。 x = (x+1)%2; p.flag = true; p.notify(); } } } } } class Output implements Runnable { //要对劳务,人这个资源进行操作,就要持有一个Person的引用。 private Person p ; public Output(Person p) { this.p = p; } @Override public void run() { while(true) { synchronized(p) { if(p.flag) { //这里就是简单的实现往工厂派发人力的过程,这里就用打印人名和姓名表示。 System.out.println(p.getName() + "<<>>"+p.getSex()); //这里也是一样判断是否为true,假如是true就直接取人并派发,派发完就把flag //设置为false,唤醒劳务送再去刷新人力,如果不是true,说明没有人力更新,就//执行wait(),将执行权交出,并好让劳务所去刷新人力。 p.flag = false; p.notify(); } else { try{p.wait();}catch(Exception e){}; } } } } }
6. 线程操作的方法如wait()和notify()、notifyAll()等方法为什么要定义在Object中类中呢?
A. 首先这些方法明确规定,这些方法需要由一个监视器或者说一个锁来调用。
B. 而且作用在当前或者其他的使用同一个监视器的线程上,比如说p.wait()它只能作用在当前使用p这个监视器的线程,也就是自身。
C. 所以它的作用是导致当前的线程等待,直到其他线程调用此对象的 notify()
方法或notifyAll()
方法,notify()
唤醒在此对象监视器上等待的单个线程,notifyAll()
唤醒在此对象监视器上等待的所有线程,由此看得出这些方法必须在同一个对象监视器上(锁)才能发挥其相应作用。
D. 那定义在Object类中,是因为在声明同步块或者同步方法的时候,会设定某一个特定的或者一个任意的对象(对象监视器),而这些对象又继承了Object类,所以可以很自然就可以有这个对象的wait()、notify()
或notifyAll()
方法
7. 生產者和消費者
//如下定义了一个产品类,产品类有自己的属性,然后创建了两个Runnable线 //程类,一个为生产者Producer和消费者Consumer,这两个类持有了对产品类 //的引用,可以方法和操作产品类对象的属性,达到生产者在刷新产品的属性,//模仿了产品生产的过程,而消费者便可以取得产品的信息,模仿了产 //品被消费的过程。 packagecom.thread; public classProducerAndConsumer { public static void main(String[] args) { Product p = new Product(); Producer pr = new Producer(p); Consumer cr = new Consumer(p); Thread t1 = new Thread(pr); Thread t2 = new Thread(pr); Thread t3 = new Thread(cr); Thread t4 = new Thread(cr); t1.start(); t2.start(); t3.start(); t4.start(); } } //定一个产品类 class Product { private String name; private int ID = 1; public boolean flag = false; public synchronized void setProperty(String name) { while(flag) { try { this.wait(); } catch(Exception e) { }; } this.name =name+" "+ID++; System.out.println(Thread.currentThread().getName()+" 生产出 "+this.name); flag = true; this.notifyAll(); } public synchronized void getInfo() { //这里使用Wlile而不是if实用为while会条件会被判断两次,不会到一个产品被//消费两次,或程序挂起的状况。 while(!flag) { try { this.wait(); } catch(Exception e) { }; } System.out.println(Thread.currentThread().getName()+" 消费了 "+this.name+" o(∩_∩)o...哈哈!!!"); flag = false; //这里notifyAll()是为了防止之唤醒本对列的线程,却没有唤醒对方的线程,最 //终导致程序都进入wait状态,程序挂起。 this.notifyAll(); } } class Producer implements Runnable { private Product p; public Producer(Product p) { this.p = p; } @Override public void run() { while(true) { p.setProperty("糖果"); } } } class Consumer implements Runnable { private Product p ; publicConsumer(Product p) { this.p = p; } @Override public void run() { while(true) { p.getInfo(); } } }
8. If判断和While技巧
if语句只会在if语句的条件判断块中判断那一次,而while会在进入while之内还会再做一次判断,也就是说往一个线程进入while之后立马给wait()或sleep住了,那么等它唤醒之后,还会再去判断一次while循环条件。
9. JDK 5.0 关于线程的新特性Lock 和Condition
A. 在JDK1.4 之后,即5.0开始出现了Lock接口和Condition接口,它们用来取代了synchronized同步锁,以及在锁的基础上的一些wait(),notify()…方法,它们出现之后不仅实现旧版本的功能之外,而且还使得线程同步使用更加灵活,功能还能更广泛。
B. Condition接口的对象充当了监视器(锁)的角色,通过它可以调用和wait、notify等一样功能的方法即await()线程等待、signal()、signalAll()唤醒一个或多个被相同监视器调用await()等方法的线程。
C. 实现了本对列唤醒对方对列的效果。
实例代码:
package com.thread; importjava.util.concurrent.locks.Condition; importjava.util.concurrent.locks.ReentrantLock; public classProducerAndConsumer { public static void main(String[] args) { //创建四个线程。 Product p = new Product(); Producer pr = new Producer(p); Consumer cr = new Consumer(p); Thread t1 = new Thread(pr); Thread t2 = new Thread(pr); Thread t3 = new Thread(cr); Thread t4 = new Thread(cr); t1.start(); t2.start(); t3.start(); t4.start(); } } //定一个产品类 class Product { private String name; private int ID = 1; public boolean flag = false; //创建一个Lock对象,一个同步锁 ReentrantLock lock = new ReentrantLock(); //创建第一个Condiditi对象,用来实现监视器的await(),signal()的方法。 Condition condition1 = lock.newCondition(); //创建第二个Condiditi对象,用来实现监视器的await(),signal()的方法, //并且用来本对列唤醒对方对列使用。 Condition condition2 = lock.newCondition(); public void setProperty(String name) throws InterruptedException { lock.lock();//获得锁,类似之前的synchronized try { while(flag) { //使用condition1调用await方法是线程进入等待。 condition1.await(); } this.name =name+" "+ID++; System.out.println( Thread.currentThread().getName()+" 生产出 "+this.name); flag = true; //唤醒一个被condition2监视器设置为等待的线程,也就是对方线程。 condition2.signal(); } finally { // 无能如何都要执行释放锁的动作,这样本方或者对方线程才有使用锁的具备。、//执行资格。 lock.lock(); } } public synchronized void getInfo() throwsInterruptedException { lock.lock();//获得锁,类似之前的synchronized try { while(!flag) { //使用condition2监视器对象调用await方法是线程进入等待。 condition2.await(); } System.out.println(Thread.currentThread().getName()+" 消费了 "+this.name+" o(∩_∩)o...哈哈!!!"); flag = false; //唤醒一个被condition1监视器设置为等待的线程,也就是对方线程。 condition1.signal(); } finally { lock.unlock();// 无能如何都要执行释放锁的动作,这样本方或者对方线程才有使用锁的具备执行资格。 } } } class Producer implements Runnable { private Product p; public Producer(Product p) { this.p = p; } @Override public void run() { while(true) { try { p.setProperty("糖果"); } catch (InterruptedExceptione) { e.printStackTrace(); } } } } class Consumer implements Runnable { private Product p ; publicConsumer(Product p) { this.p = p; } @Override public void run() { while(true) { try { p.getInfo(); } catch(InterruptedException e) { e.printStackTrace(); } } } }
10. interrupted()
方法
如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或sleep(long, int) 方法过程中受阻,则再调用这个方法就会中使得断状态将被清除,它还将收到一个InterruptedException。
11. 停止线程
Tread方法中的stop方法已经过时,不建议被使用,所以当我们要停止一个线程的时候,最好采用循环控制,或条件控制的方式让线程结束run()方法。
12. 守护线程或用户线程
setDaemon(boolean on)
将该线程标记为守护线程或用户线程。该方法必须在启动线程前调用。
解释: 也就是说当一个程序中只剩下一个或者多个守护进程或者用户进程时,Java虚拟机会自动退出,也就是这些此线程会随着非守护线程存在在而存在,一旦程序中没有了非守护进程,或者程序中只剩下守护进程时程序自动退出。
13. Join()的方法
A. 当A线程执行到B线程的.join()的方法时,A会暂停执行,直到B线程执行完毕,A才会执行或有可能继续执行。
B. join可以用来临时加入线程执行。
代码1:
public static void main(String[] args) { Person p = new Person(); Input in = new Input(p); Output out = new Output(p); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); //t1调用了join方法,当住方法执行到这句话的时候,主程序会停下来,等待t1 //执行完毕,在继续执行。 t1.join(); t2.start(); }
代码2:
public static void main(String[]args) { Person p = new Person(); Input in = new Input(p); Output out = new Output(p); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); //t1调用了join方法,当主方法执行到这的时候,线程t1和t2会交替执行,但是此//ain方法会停住,且会等待t1执行完毕之后在恢复执行。 t1.join(); }
14. Yield()方法、
让一个线程暂停,执行其他的线程….老师说这个方法用的不是太多,而且用起来也比较简单,就不多说了,要看其他的课程了,写的不好,请大家担待,可能只有在自己看,自己复习的时候才有效果吧,哈哈,对不住了各位。