一、线程同步和死锁问题
异步问题:
package com.horizon.action; /** * 测试同步问题 * */ public class TestSync { public static void main(String[] args) { Account a1 = new Account(100, "高"); Drawing draw1 = new Drawing(80, a1); Drawing draw2 = new Drawing(80, a1); draw1.start(); // 你取钱 draw2.start(); // 你老婆取钱 } } /* * 简单表示银行账户 */ class Account { int money; String aname; public Account(int money, String aname) { super(); this.money = money; this.aname = aname; } } /** * 模拟提款操作 * * @author Administrator * */ class Drawing extends Thread { int drawingNum; // 取多少钱 Account account; // 要取钱的账户 int expenseTotal; // 总共取的钱数 public Drawing(int drawingNum, Account account) { super(); this.drawingNum = drawingNum; this.account = account; } @Override public void run() { if (account.money - drawingNum < 0) { return; } try { Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行 } catch (InterruptedException e) { e.printStackTrace(); } account.money -= drawingNum; expenseTotal += drawingNum; System.out.println(this.getName() + "--账户余额:" + account.money); System.out.println(this.getName() + "--总共取了:" + expenseTotal); } }
结果是:
Thread-0--账户余额:-60
Thread-1--账户余额:-60
Thread-0--总共取了:80
Thread-1--总共取了:80
问题分析:
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。
由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块:
1. synchronized 方法:通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:
public synchronized void accessVal(int newVal);(同步的方法必须获得对象的锁才能运行;普通方法不获得锁也可以运行)
synchronized 方法控制对类成员变量的访问:
每个对象对应一把锁,每个
synchronized
方法都必须获得调用该方法的对象的锁方能执行(跟synchronize(this)意思差不多),否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
synchronized
方法的缺陷:
若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。
2. synchronized
块:通过 synchronized关键字来声明synchronized 块。语法如下:
synchronized(syncObject)
{
//允许访问控制的代码
}
实例代码:
package com.horizon.action; /** * 测试同步问题 */ public class TestSync { public static void main(String[] args) { Account a1 = new Account(100, "高"); Drawing draw1 = new Drawing(80, a1); Drawing draw2 = new Drawing(80, a1); draw1.start(); // 你取钱 draw2.start(); // 你老婆取钱 } } /* * 简单表示银行账户 */ class Account { int money; String aname; public Account(int money, String aname) { super(); this.money = money; this.aname = aname; } } /** * 模拟提款操作 * * @author Administrator * */ class Drawing extends Thread { int drawingNum; // 取多少钱 Account account; // 要取钱的账户 int expenseTotal; // 总共取的钱数 public Drawing(int drawingNum, Account account) { super(); this.drawingNum = drawingNum; this.account = account; } @Override public void run() { draw(); } void draw() { synchronized (account) { if (account.money - drawingNum < 0) { return; } try { Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行。 } catch (InterruptedException e) { e.printStackTrace(); } account.money -= drawingNum; expenseTotal += drawingNum; } System.out.println(this.getName() + "--账户余额:" + account.money); System.out.println(this.getName() + "--总共取了:" + expenseTotal); } }
上面这种方式叫做:互斥锁原理。利用互斥锁解决临界资源问题。
二、死锁及解决方案:
死锁问题:
package com.horizon.action; class Lipstick { } class Mirror { } class Makeup extends Thread { int flag; String girl; // 注意是static,这样才能使两个线程得到的是同一个数据 static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); @Override public void run() { // TODO Auto-generated method stub doMakeup(); } void doMakeup() { if (flag == 0) { synchronized (lipstick) { System.out.println(girl + "拿着口红!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (mirror) { System.out.println(girl + "拿着镜子!"); } } } else { synchronized (mirror) { System.out.println(girl + "拿着镜子!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lipstick) { System.out.println(girl + "拿着口红!"); } } } } } public class TestDeadLock { public static void main(String[] args) { Makeup m1 = new Makeup(); m1.girl = "大丫"; m1.flag = 0; Makeup m2 = new Makeup(); m2.girl = "小丫"; m2.flag = 1; m1.start(); m2.start(); } }
如何解决死锁问题:
1. 往往是程序逻辑的问题,需要修改程序逻辑。
2. 尽量不要同时持有两个对象锁。如修改成如下:
package com.horizon.action; class Lipstick { } class Mirror { } class Makeup extends Thread { int flag; String girl; // 注意是static,这样才能使两个线程得到的是同一个数据 static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); @Override public void run() { // TODO Auto-generated method stub doMakeup(); } void doMakeup() { if (flag == 0) { synchronized (lipstick) { System.out.println(girl + "拿着口红!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }
// 不放在上面同步块里面。不同时持有两个锁 synchronized (mirror) { System.out.println(girl + "拿着镜子!"); } } else { synchronized (mirror) { System.out.println(girl + "拿着镜子!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (lipstick) { System.out.println(girl + "拿着口红!"); } } } } public class TestDeadLock { public static void main(String[] args) { Makeup m1 = new Makeup(); m1.girl = "大丫"; m1.flag = 0; Makeup m2 = new Makeup(); m2.girl = "小丫"; m2.flag = 1; m1.start(); m2.start(); } }
解决多线程问题的另外一种思路是同步。
同步是另外一种解决问题的思路,结合前面卫生间的示例:
互斥方式解决多线程的原理是,当一个人进入到卫生间内部时,别的人只能在外部时刻等待,这样就相当于别的人虽然没有事情做,但是还是要占用别的人的时间,浪费系统的执行资源。
而同步解决问题的原理是,如果一个人进入到卫生间内部时,则别的人可以去睡觉,不占用系统资源,而当这个人从卫生间出来以后,把这个睡觉的人叫醒(就是wait,notify的应用),则它就可以使用临界资源了。所以使用同步的思路解决多线程问题更加有效,更加节约系统的资源。
在常见的多线程问题解决中,同步问题的典型示例是“生产者-消费者”模型,也就是生产者线程只负责生产,消费者线程只负责消费,在消费者发现无内容可消费时则睡觉同步解决问题的另一种典型方式:
生产者/消费者
模式: (生产者和消费者共享了SyncStack对象。一个从里面拿东西,一个从里面消费东西)
下面举一个例子:
package com.horizon.action; public class TestProduce { public static void main(String[] args) { SyncStack sStack = new SyncStack(); Shengchan sc = new Shengchan(sStack); Xiaofei xf = new Xiaofei(sStack); sc.start(); xf.start(); } } class Mantou { int id; Mantou(int id) { this.id = id; } } class SyncStack { int index = 0; Mantou[] ms = new Mantou[10]; public synchronized void push(Mantou m) { while (index == ms.length) { try { this.wait(); // wait后,线程会将持有的锁释放。sleep是即使睡着也持有互斥锁。 } catch (InterruptedException e) { e.printStackTrace(); } } this.notify(); // 唤醒在当前对象等待池中等待的一个线程(随意的)。notifyAll叫醒所有在当前对象等待池中等待的所有线程。 // 如果不唤醒的话。以后这两个线程都会进入等待线程,没有人唤醒。 ms[index] = m; index++; } public synchronized Mantou pop() { while (index == 0) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.notify(); index--; return ms[index]; } } class Shengchan extends Thread { SyncStack ss = null; public Shengchan(SyncStack ss) { // TODO Auto-generated constructor stub this.ss = ss; } @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 20; i++) { System.out.println("造馒头:" + i); Mantou m = new Mantou(i); ss.push(m); } } } class Xiaofei extends Thread { SyncStack ss = null; public Xiaofei(SyncStack ss) { // TODO Auto-generated constructor stub this.ss = ss; } @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 20; i++) { Mantou m = ss.pop(); System.out.println("吃馒头:" + i); } } }
三、线程回顾总结
1. New:创建好线程对象,但没有启动的时候。
一个线程调用start()之后不一定会马上启动,此时进入就绪状态,等待得到资源。
2.
就绪线程序通过Scheduler(调度程序)去确定是否运行。
Runing---dead:运行结束(非双向,为单向箭头)
Runing---就绪:暂停(除了没有CPU,具备运行的所有条件)
Runing-otherwise(阻塞):因程序原因:调用sleep或join之后,线程被阻塞。这时不具备运行的条件,此时线程进入阻塞池.sleep或join条件解除之后直接进入Runnable不进入running。
3.
Lock pool:锁池状态。每个对象都有自己的锁池,锁池里放置了想获得对象锁的线程。
等待状态(wait
pool):比如一个线程调用了某个对象的wait()方法,就进入了该对象的wait pool,
正在等待其它线程调用这个对象的notify()或者notifyAll()(这两个方法同样是继承自Object类)方法来唤醒它。