synchronized 锁住当前对象的理解
public class TT implements Runnable { int b = 100; public synchronized void m1() throws Exception{ //Thread.sleep(2000); b = 1000; Thread.sleep(5000); System.out.println("b = " + b); } public void m2() throws Exception { Thread.sleep(2000); b = 2000; } public void run() { try { m1(); } catch(Exception e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { TT tt = new TT(); Thread t = new Thread(tt); t.start(); tt.m2(); } }
m1()方法和m2()方法都访问了成员变量b,m1有加锁(对调用m1方法的对象加锁) m2没有加锁
打印结果:b=2000 tt.m2()方法并没有等t线程执行结束就访问了m1() t.start()和tt.m2()并没有同步
结论:
类的这个对象,是一个资源
这个资源能不能好好的被访问,就像账户里的钱 能不能保证前后访问一致
你必须把访问这个资源的所有的方法,都要考虑到是否设置成同步的(加锁)
锁住当前对象只是针对加了synchronized 关键字m1()这段方法,另外一个线程绝对不可能执行这段代码,但是有可能执行其他的代码m2()
模拟生产者与消费者
生产者和消费者例子中,蕴含了多线程很多个知识点
启动线程、 synchronized关键字sleep 、 wait 和notify的用法 以及数据结构中的栈
package javaee.net.cn.thread; public class ProducerConsumer { //这里用是三个生产者线程和一个消费者线程来测试 //如果想要把生产者生产的全部消费完 消费者的run方法执行的次数就需要时生产者的三倍 (i<6) public static void main(String[] args) { SyncStack ss = new SyncStack(); Producer p = new Producer(ss); Consumer c = new Consumer(ss); //启动线程是调用start()方法,如果直接调用run方法就是普通的方法调用,不是异步的。 new Thread(p).start(); new Thread(p).start(); new Thread(p).start(); new Thread(c).start(); } } //这是声明一个对象 用来放进栈里面 class WoTou { int id; WoTou(int id) { this.id = id; } public String toString() { return "WoTou : " + id; } } /** * 数据结构中 栈 * 有push和pop方法 */ class SyncStack { int index = 0; WoTou[] arrWT = new WoTou[6]; //因为arrWT 和index是成员变量,所以需要对这两个方法加锁。 加锁是锁住当前对象,他里面的成员变量自然也就锁定了 public synchronized void push(WoTou wt) { //须用while 而不是if 因为如果栈满了 需要一直等待 while(index == arrWT.length) { try { //栈满了时 需要停止住 this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //wait不同于sleep 不需要指定睡眠的时间, 但需要手动唤醒. 当index不等arrWT.length 表示栈现在不是满的可以进行push 于是手动唤醒线程 this.notifyAll(); //如果不加锁 arrWT和index因为多线程同步 导致新加的窝头和指针不匹配 //先把当前指针 执行新加的窝头 arrWT[index] = wt; //然后把指针加1 index ++; } //pop操作和push操作刚好相反 public synchronized WoTou pop() { while(index == 0) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.notifyAll(); index--; return arrWT[index]; } } /** * 定义一个生产者 每生产一个对象 睡眠200ms */ class Producer implements Runnable { SyncStack ss = null; Producer(SyncStack ss) { this.ss = ss; } @Override public void run() { for(int i=0; i<2; i++) { WoTou wt = new WoTou(i); ss.push(wt); System.out.println("生产了:" + wt); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 定义一个消费者 */ class Consumer implements Runnable { SyncStack ss = null; Consumer(SyncStack ss) { this.ss = ss; } @Override public void run() { for(int i=0; i<6; i++) { WoTou wt = ss.pop(); System.out.println("消费了: " + wt); try { //每消费一个对象 睡眠200ms 可以在console控制台 慢慢的看到线程的调度执行 Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } }
生产了:WoTou : 0
生产了:WoTou : 0
生产了:WoTou : 0
生产了:WoTou : 1
消费了: WoTou : 1
消费了: WoTou : 1
消费了: WoTou : 1
生产了:WoTou : 1
生产了:WoTou : 1
消费了: WoTou : 0
消费了: WoTou : 0
消费了: WoTou : 0
这是三个生产者一个消费者 最后的打印结果,可见 生产的窝头全部都被消费了
补充 volatile关键字和syschronized
synchronized声明不会被继承,如果一个用synchronized修饰的方法被子类覆盖,那么子类中的这个方法不再保持同步,除非也用synchronized修饰(锁重入)
synchonized 最能用来锁定字符串常量(因为常量池里面只有一个对象 如果允许,各种第三方jar包都锁定了同一个对象),也不能锁定基础的数据类型
补充:程序之中如果出现了异常,锁会被释放,会被其他程序冲进来(程序乱入,导致数据不一致 访问到异常产生中间的数据)
解决办法:Catch住异常
线程的常用方法和关键字
wait()和join()方法都会释放锁(join方法的底层也是使用了wait方法)
wait:的目的是为了多个线程抢资源的时候,当前线程把资源让出来
如果没有锁,就不存在两个线程抢资源的情况,也就没有必要把资源让出来,两个线程早就一起跑了 这也很好解释了为什么wait方法要和synchronized关键字连用。
join():合并某个线程,他调用此方法的线程(别的线程)合并到当前线程上来。等待调用此方法的线程执行完了,当前线程才开始继续执行(经常用于等待另外一个线程的结束,有点像方法调用)。
interrupt():中断阻塞会释放当前锁(如执行System.in.read() 也会进入阻塞状态等待用户的输入、调用了其他线程的join()方法、等待获取某个对象的锁 如死锁、wait())等都会进入阻塞状态。
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的
sleep()和yield()方法,当前线程都会放弃CPU,并且都不会释放锁
sleep():的过程之中 如果对当前对象加锁了(可以不加),不会释放当前对象的锁。
yield(): 当前线程会做出让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示
----------------------------------------------------------------------------------------------
notify():当一个线程执行了s.notify()后,如果在对象s上等待有许多个线程,那么java虚拟机随机取出一个线程,把它放到对象s的锁池去竞争获取锁的机会;
如果对象s的锁池中没有任何线程,那么notify()方法什么也不做。
等待池:假如线程A调用了某个对下的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该线程的等待池中,进入到等待池中的线程不会竞争到该对象的锁。
锁池: 假如线程A已经拥有了某个对下的锁,而其他线程B,C想要调用这个对下的某个Synchronized方法,
此时B,C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。
JAVA多线程死锁
产生死锁的原因:
不同的线程等待不可能被释放的锁 互相等待对方释放锁
看一个死锁的例子,
/** * 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步 * 或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的 */ public class TestDeadLock implements Runnable { public int flag = 1; private static Object o1 = new Object(); private static Object o2 = new Object(); public void run() { System.out.println("flag=" + flag); if(flag == 1) { synchronized(o1) { try { //睡眠500ms,让t2线程执行先锁住o2 Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } //因为t1线程睡眠了500ms t1执行到这里的时,o2已经被 t2锁住了 synchronized(o2) { System.out.println("死锁住了1"); } } } if(flag == 2) { synchronized(o2) { synchronized(o1) { System.out.println("死锁住了2"); } } } } public static void main(String[] args) { TestDeadLock td1 = new TestDeadLock(); TestDeadLock td2 = new TestDeadLock(); td1.flag = 1; td2.flag = 2; Thread t1 = new Thread(td1); Thread t2 = new Thread(td2); t1.start(); t2.start(); } }
上面代码执行的流程是,线程t1和线程t2异步执行。 执行线程t1的时 先锁住o1 此时线程t1 睡眠500ms。t2线程开始执行,t2线程锁住o2
等t2线程执行的时候 需要拿到o1的锁才能执行下去,此时1线程往下执行的时候 需要拿到o2的锁才能继续执行
t1线程和t2线程互相持有对方锁(t1锁住了o1 拿到o2的锁就可以继续执行了,t2锁住了o2,拿到o1的锁就可以继续执行了) 陷入了死锁的状态。
死锁解决办法
死锁是程序设计的Bug 在设计程序时就要避免双方互相持有对方锁的情况,只要互相等待对方释放锁就有可能出现死锁
当几个线程都需要访问共享资源A B和C时,保证每个线程都以相同的顺序执行他们,比如都先访问A,在访问B和C。
concurrent并发包
Lock
ReentrantLocak和sysnchronized的区别
支持异步的Callable接口和Future接口
1)Callable接口:它和Runnable接口有点类似,都指定了线程所需要执行的操作。区别在于,Callable接口是在call()方法中指定线程所要执行的操作的,并且该方法有泛型的返回值<V>。
此外 Callable实例不能像Runnable实例那样,直接作为Thread类的构造方法的参数。
2)Future接口:能够保存异步运算的结果。
get():方法返回异步运算的结果。如果运算结果还没处理,当前线程就会被阻塞,直到获得运算结果,才结束阻塞。
下面程序演示两个线程之间异步运算的过程。
public class Machine implements Callable<Integer>{ @Override public Integer call() throws Exception { int sum=0; for(int a=0;a<100;a++){//计算从1加到100 sum=sum+a; Thread.sleep(20); } return sum; } public static void main(String[] args) throws Exception { FutureTask<Integer> task=new FutureTask<Integer>(new Machine()); Thread threadMachine = new Thread(task); threadMachine.start();// 执行Machine的call()方法 System.out.println("等待计算结果..."); //主线程调用task.get()方法 获得运算结果 System.out.println("从1加到100的和:"+task.get()); System.out.println("计算完毕"); } }
在以上程序中,Machine类实现了Callable接口,threadMachine线程负责执行Machine类的call()方法,该方法会计算从1加到100的和,并且返回运算的结果。
主线程调用task.get()方法,当ThreadMachine线程还没运算完毕时,主线程就会阻塞,直到threadMachine线程执行完call()方法,主线程才会获得运算结果,并从task.get()方法中退出。