死锁
死锁通常是同步中嵌套同步,但是线程的锁却不同。如下代码:
/**
* 提供锁对象,锁对象静态初始化可以直接类名访问
*/
class MyLock{
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLock implements Runnable{
private boolean flag;
public DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
while(true){
if (flag) {
// 同步进行了嵌套,但是持有的锁却不同
synchronized (MyLock.locka) {
System.out.println("当前运行的线程:" + Thread.currentThread().getName() + ",if中的locka");
synchronized (MyLock.lockb) {
System.out.println("当前运行的线程:" + Thread.currentThread().getName() + ",if中的lockb");
}
}
} else {
synchronized (MyLock.lockb) {
System.out.println("当前运行的线程:" + Thread.currentThread().getName() + ",else中的lockb");
synchronized (MyLock.locka) {
System.out.println("当前运行的线程:" + Thread.currentThread().getName() + ",else中的locka");
}
}
}
}
}
}
class DeadLockDemo {
public static void main(String[] args) {
Thread t1 = new Thread(new DeadLock(true));
Thread t2 = new Thread(new DeadLock(false));
t1.start();
t2.start();
}
}
在以上的程序中,线程0拿到了a锁,同时线程1拿到了b锁,线程0请求b锁,线程1请求a锁,但是线程0和线程1的第一个同步块都没有执行完,根本可能释放各自的锁,因而造成了死锁。
线程间通讯实际上就是多个线程操作同一个资源,但是操作的动作不同。
生产者和消费者
生产者线程不断生产资源,消费者线程不断取走资源。生产者生产商品和消费者取走商品是交替进行的。生产者与消费者需要用到同步机制和等待和唤醒机制。
/**
* 资源
*/
class Resource{
private String country;
private String language;
// 可以用单例设计模式产生该资源的唯一实例,也可以将该资源实例的引用传递给不同的对象
// private static final Resource res = new Resource();
// public static Resource getInstance(){
//
// return res;
// }
//
// private Resource() {
//
// }
public boolean canGet = false; // 资源初始值是可以生产,不能取走
public synchronized void putResuorce(String country,String language) throws InterruptedException{
if (canGet) { // 可以取走,不能生产,生产者线程必须等待
super.wait();
}
this.country = country;
this.language = language;
canGet = true; // 生产者已经生产过一次了,置标记为可以取走但是不能生产
super.notify(); // 唤醒等待线程(消费者)
}
public synchronized void getResuorce() throws InterruptedException{
if (!canGet) {
super.wait();
}
System.out.println(country + "---->" + language);
canGet = false;
super.notify();
}
}
/**
* 生产者线程
*/
class Producer implements Runnable{
// private Resource res = Resource.getInstance();
private Resource res = null;
boolean isChina = true;
public Producer(Resource res) {
this.res = res;
}
@Override
public void run() {
for(int i = 0; i < 50;i++){
if(isChina){
try {
res.putResuorce("中国", "汉语");
Thread.sleep(50);
isChina = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
try {
res.putResuorce("American", "English");
isChina = true;
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 消费者线程
*/
class Consumer implements Runnable{
// private Resource res = Resource.getInstance();
private Resource res = null;
public Consumer(Resource res) {
this.res = res;
}
@Override
public void run() {
for(int i = 0; i < 50;i++){
try {
res.getResuorce();
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ProducerConsumerTest {
public static void main(String[] args) {
Resource res = new Resource();
new Thread(new Producer(res)).start(); // 生产者线程
new Thread(new Consumer(res)).start(); // 消费者线程
// new Thread(new Producer()).start();
// new Thread(new Consumer()).start();
}
}
在以上的程序中生产者生产50个产品,消费者取得50个产品。有以下几点需要注意:
- 生产者的生产方法和消费者的取走方法是原子操作,不能打断,需要同步;
- 给资源设置一个标记,当线程切换到生产者线程的时候首先判断是否可以进行生产,如果可以生产则进行生产,生产完毕之后改变资源的标记为可以取走但是不能生产,唤醒等待线程(消费者线程);如果生产者线程判断的标记为不能生产但是可以取走则该生产者线程必须等待;同理当线程切换到消费者线程的时候首先判断资源的标记,如果资源标记为可以取走则进行取走,改变标记为可以生产但是不能取走,唤醒等待线程(消费者线程),如果消费者线程判断资源的标记是可以生产但是不能取走,则该消费者线程必须等待(等待生产者完成生产之后唤醒该消费者)。
- 以上的程序仅仅使用于1个生产者和一个消费者。
线程运行的时候会在内存中建立一个
线程池,等待的线程都会被放在线程池中。notify()方法会任意唤醒线程池中的一个等待线程,notifyAll()方法唤醒线程池中的所有等待的线程。
API文档中的
对象监视器指的是锁,而只有同步才会有锁。因此wait()、notify()、notifyAll()方法都用在同步上,并且必须标识他们持有的锁(监视器),而监视器可以是任意类的对象,所以这些方法定义在Object类中。
只有同一个锁上的等待线程可以被同一个锁上的notify()方法所唤醒,不可以对持有不同锁的线程进行唤醒——也就是说等待和唤醒必须是同一把锁。在以上的代码中锁是this(Resource类的实例),而传递给生产者和消费者的是同一个Resource实例。
带编号的生产者和消费者:
/**
* 资源
*/
class Resource{
private String name;
private int count = 1;
private boolean canSet = true;
/**
* 生产商品(该方法需要同步),并需要等待和唤醒
*/
public synchronized void set(String name){
if(!canSet){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name +"_" + count++;
System.out.println(Thread.currentThread().getName() + "生产商品--->" + this.name);
canSet = false;
notify();
}
/**
* 取出商品(该方法需要同步),并需要等待和唤醒
*/
public synchronized void get(){
if(canSet){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消费商品<---" + this.name);
canSet = true;
notify();
}
}
/**
* 生产者线程
*/
class Producer implements Runnable{
Resource res;
public Producer(Resource res) {
this.res = res;
}
@Override
public void run() {
for(int i = 0;i< 50;i++){
res.set("商品");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消费者线程
*/
class Consumer implements Runnable{
Resource res;
public Consumer(Resource res) {
this.res = res;
}
@Override
public void run() {
for(int i = 0;i < 50;i++){
res.get();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ProducerConsumerTest {
public static void main(String[] args) {
Resource res = new Resource();
new Thread(new Producer(res)).start();
new Thread(new Consumer(res)).start();
}
}
多个生产者和消费者:
如果直接将以上的代码改变成多个生产者和消费者,即:在main方法中进行以下的调用:
new Thread(new Producer(res)).start();
new Thread(new Producer(res)).start();
new Thread(new Consumer(res)).start();
new Thread(new Consumer(res)).start();
以上实现的生产者和消费者有一个潜在的问题:
仅仅适用于一个生产者和一个消费者的情况。如果生产者和消费者有多个就可能出现生产商品和消费商品的数目不匹配的问题。针对以上程序的结果做以下的分析:
线程0和线程1是两个生产者线程、线程2和线程3是两个消费者线程。假设在某一时刻线程池中等待的线程是线程2、线程3,线程0获得了处理机,设置商品的编号为57,商品编号设置完成之后线程0就进入了等待状态,此时的线程池中的线程是:线程2、线程3、线程0。线程0设置好商品编号之后从线程池中唤醒第一个等待线程:线程2,取出57号商品,设置资源的标记是不可取出的,然后唤醒线程池中的第一个等待线程:线程3.
但是线程3并不判断资源的标记(尽管资源的标记是不可取出的),它仍然向下执行又取出了一次57号商品!——其根本原因是线程3原先就在if块中等待,而if只能进行一次判断,线程被唤醒之后继续向下执行,并不进一步判断资源的状态!
解决方案:将if块改造成while。
/**
* 资源
*/
class Resource{
private String name;
private int count = 1;
private boolean canSet = true;
/**
* 生产商品(该方法需要同步),并需要等待和唤醒
*/
public synchronized void set(String name){
while(!canSet){ // 将if改造成while,每次唤醒之后都要判断资源状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name +"_" + count++;
System.out.println(Thread.currentThread().getName() + "生产商品--->" + this.name);
canSet = false;
notify();
}
/**
* 取出商品(该方法需要同步),并需要等待和唤醒
*/
public synchronized void get(){
while(canSet){ // 将if改造成while,每次唤醒之后都要判断资源状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消费商品<---" + this.name);
canSet = true;
notify();
}
}
但是以上的问题依然没有全部解决,运行程序会出现
全部等待的情况。
解决方案:将notify换成notifyAll方法
/**
* 资源
*/
class Resource{
private String name;
private int count = 1;
private boolean canSet = true;
/**
* 生产商品(该方法需要同步),并需要等待和唤醒
*/
public synchronized void set(String name){
while(!canSet){ // 将if改造成while,每次唤醒之后都要判断资源状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name +"_" + count++;
System.out.println(Thread.currentThread().getName() + "生产商品--->" + this.name);
canSet = false;
notifyAll(); // 唤醒全部
}
/**
* 取出商品(该方法需要同步),并需要等待和唤醒
*/
public synchronized void get(){
while(canSet){ // 将if改造成while,每次唤醒之后都要判断资源状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消费商品<---" + this.name);
canSet = true;
notifyAll(); // 唤醒全部,如果是本方进入while循环则会等待
}
}
总结:多个生产者和消费者应该使用while循环判断资源的读取/设置状态,并使用notifyAll方法。
JDK1.5之后的生产者和消费者
java.util.concurrent.locks.Lock接口。提供了比使用synchronized和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。
锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock 的读取锁。
synchronized
方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。
虽然 synchronized
方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用 "hand-over-hand" 或 "chain locking":获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。Lock
接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。
Condition
将 Object
监视器方法(wait
、notify
和 notifyAll
)分解成截然不同的对象,以便通过将这些对象与任意 Lock
实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock
替代了 synchronized
方法和语句的使用,Condition
替代了 Object 监视器方法的使用。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* JDK1.5的生产者和消费者和消费者的升级解决方案
* 使用Lock接口的lock方法替代了synchronized
* 使用Condition中的await和signal方法替换了Object类中的wait和notify方法
* Condition对象可以通过Lock实例获得
* 在该类中实现了本方仅仅唤醒对方的操作
*/
class Resource{
private boolean canPut = true;
private String name;
private int count = 1;
private final Lock lock = new ReentrantLock(); // 锁
// private final Condition condition = lock.newCondition(); // Condition对象
private final Condition condition_pro = lock.newCondition();// 消费者Condition对象
private final Condition condition_con = lock.newCondition();// 生产者Condition对象
public void put(String name) throws InterruptedException {
lock.lock(); // 获得锁
try {
while(!canPut){
// condition.await();
condition_pro.await(); // 生产者线程等待
}
this.name = name + "_" +count++;
System.out.println("生产者" + Thread.currentThread().getName() + "生产商品---->" + this.name);
canPut = false;
// condition.signalAll(); // 唤醒等待线程
condition_con.signal(); // 唤醒消费者线程
} finally{
lock.unlock(); // 释放锁
}
}
public synchronized void get() throws InterruptedException{
lock.lock();
try {
while(canPut) {
// condition.await();
condition_con.await(); // 消费者线程等待
}
System.out.println("消费者" + Thread.currentThread().getName() + "消费产品<----" + this.name);
canPut = true;
// condition.signalAll();
condition_pro.signal(); // 唤醒生产者线程
} finally{
lock.unlock();
}
}
}
class Producer implements Runnable{
private Resource res;
public Producer(Resource res) {
this.res = res;
}
@Override
public void run() {
while(true)
try {
res.put("商品");
Thread.sleep(80);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable{
private Resource res;
public Consumer(Resource res) {
this.res = res;
}
@Override
public void run() {
while(true)
try {
res.get();
Thread.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ProducerConsumerTest{
public static void main(String[] args) throws InterruptedException {
Resource res = new Resource();
new Thread(new Producer(res)).start();
new Thread(new Producer(res)).start();
new Thread(new Consumer(res)).start();
new Thread(new Consumer(res)).start();
}
}
升级之后一个锁可以对应多个Condition。
如果有静态的方法可以对静态变量的状态作更新,还能够用同步化么?
可以。静态的方法是运行在类上而不是每个实例上。所以你可能猜想要用哪个对象的锁。毕竟有可能完全没有该类的实例存在。幸好对象有锁,每个被载入的类也有个锁。这表示说:如果有3个是Dog实例的,一个是类的。当你要对静态的方法做同步化时,Java会使用类本身的锁。因此如果一个类有两个被同步化过的静态方法,线程需要取得类的锁才能进入这些方法。
为什么不把要被保护的数据的getter和setter同步化?
事实上,我们应该要把这些方法都同步化,以防止其他的线程以别种方式存取它们。但是仅仅将getter和setter同步化是不够的。同步化的意义是指定某段工作要在不能分割的状态下执行。也就是说单独的操作不重要,重要的是有多个操作的方法。因此把所有的存取的方法都同步化是个好主意,但还是得要把不可分割的原子操作同步化。
线程的停止
线程的停止只有一种情况:
run()方法结束。而run()方法多数是循环结构,只要控制循环结束就可以让run()结束(线程结束)。见如下的代码:
class MyThread implements Runnable{
boolean isRun = true; // 是否停止线程的标记
@Override
public void run() {
while (isRun) {
System.out.println(Thread.currentThread().getName() + "运行");
}
}
public void stopMyThread(){
isRun = false;
}
}
public class ThreadStopTest{
public static void main(String[] args) {
int count = 0;
MyThread myThread = new MyThread();
new Thread(myThread).start();
new Thread(myThread).start();
while (true) {
if(count++ == 50){
myThread.stopMyThread(); // 停止线程,改变标记
break;
}
}
}
}
但是下面这种情况
不能仅仅使用改变标记位的方式停止线程。
class MyThread implements Runnable {
boolean isRun = true;
@Override
public synchronized void run() {
while (isRun) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行");
}
}
public void stopMyThread() {
isRun = false;
}
}
public class ThreadStopTest {
public static void main(String[] args) {
int count = 0;
MyThread myThread = new MyThread();
new Thread(myThread).start();
new Thread(myThread).start();
while (true) {
if (count++ == 50) {
myThread.stopMyThread();
break;
}
}
System.out.println(Thread.currentThread().getName() + "执行完毕!");
}
}
运行以上程序,则所有的线程进入run()方法都会等待,程序不会结束。
改变了标记,但是线程并没有终止(原因就是线程处于了冻结状态——即API文档中所说的中断状态)。
所谓的清除中断状态就是将线程从冻结状态转换到运行状态,抛出
InterruptedException。
class MyThread implements Runnable{
boolean isRun = true;
@Override
public synchronized void run() {
while (isRun) {
try {
wait();
} catch (InterruptedException e) { // 只要发生的异常就代表有人强制使它中断,结束线程
// e.printStackTrace();
isRun = false;
}
System.out.println(Thread.currentThread().getName() + "运行");
}
}
public void stopMyThread(){
isRun = false;
}
}
public class ThreadStopTest{
public static void main(String[] args) {
int count = 0;
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
Thread t2 = new Thread(myThread);
t1.start();
t2.start();
while (true) {
if(count++ == 50){
t1.interrupt(); // 线程处于冻结状态,先清除它的中断状态,抛出中断异常,在异常处理代码中改变标记
t2.interrupt();
break;
}
}
System.out.println(Thread.currentThread().getName() + "执行完毕!");
}
}
总结:只要程序处于冻结状态就可以使用interrupt()方法清除其中断状态,强制使线程恢复到运行状态中来,这样就可以通过操作标记位让线程结束。
守护线程
守护线程可以理解为后台线程,当所有的前台线程结束后,守护线程就自动结束——这是守护线程和前台线程的区别。
class MyThread implements Runnable{
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "运行"); // 所有前台线程运行完毕之后自动结束
}
}
}
public class DaemonThreadTest{
public static void main(String[] args) {
int count = 0;
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
Thread t2 = new Thread(myThread);
t1.setDaemon(true); // 设置守护线程
t2.setDaemon(true);
t1.start();
t2.start();
while (true) {
if(count++ == 50){
break;
}
}
System.out.println(Thread.currentThread().getName() + "执行完毕!");
}
}
线程的强制运行和礼让
join方法可以强制获得CPU的执行权。
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
Thread t2 = new Thread(myThread);
t1.start();
t2.start();
t1.join();// 线程强制运行
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
System.out.println(Thread.currentThread().getName() + "结束!");
}
}
调整以上代码的顺序:
t1.start();
t2.start();
t1.join(); // 线程强制运行
运行结果:
当A线程执行到了B线程的.join()方法时,A就会等待。等B线程都执行完,A才会执行。join可以用来临时加入线程执行。t1和t2两个线程交替执行,主线程要等待t1结束。
线程的优先级以及yield方法
线程组:例如主线程开启了t1和t2两个线程,那么t1和t2的线程组就是main。我们调用线程的toString()方法就会打印线程名称,优先级和线程组。
所有的线程的默认优先级是5。可以通过
setPriority方法设置优先级。
class MyThread implements Runnable{
@Override
public void run() {
for(int i = 0;i < 10;i++){
System.out.println(Thread.currentThread() + "---->" + i);
}
}
}
public class ThreadPriorityTest{
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
Thread t2 = new Thread(myThread);
t1.start();
t1.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级
t2.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread() + "---->" + i);
}
System.out.println(Thread.currentThread() + "结束!");
}
}
设置了优先级并不是代表它一定能够得到处理机,而是抢占CPU的几率高了一些。
yield方法
class MyThread implements Runnable{
@Override
public void run() {
for(int i = 0;i < 10;i++){
System.out.println(Thread.currentThread() + "---->" + i);
Thread.yield(); // 线程的礼让
}
}
}
yield方法用于将处理机资源礼让出去。主线程开启的两个线程t1和t2得到处理机后立即礼让,主线程就得到了处理机。主线程执行结束后。t1和t2互相礼让,最终t1和t2的执行比较平均.