java 多线程-3
十、同步机制解决Thread继承安全问题
创建三个窗口买票,共100张票。用继承来实现
-
方式一:同步代码块
public class RunMainExtends { public static void main(String[] args) { Win win1 = new Win(); Win win2 = new Win(); Win win3 = new Win(); // 设置线程的名字 win1.setName("窗口一:"); win2.setName("窗口二:"); win3.setName("窗口三:"); // 开启线程 win1.start(); win2.start(); win3.start(); } } /* 方式一:同步代码块*/ class Win extends Thread { /* 注意:ticket、object必须加static。因为实例化的三个线程对象,是不同的对象,他们各自有自己的栈和程序计数器。只有加了static才能让这个三个对象共享ticket、object。 */ private static int ticket = 100; private static Object object = new Object(); @Override public void run() { while (true) { synchronized (object){// 同步代码块,同步监视器,必须是同一把锁 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"获取到了第"+ticket+"票"); ticket--; }else{ break; } } } } }
-
方式二:同步方法
public class RunMainExtends { public static void main(String[] args) { Win win1 = new Win(); Win win2 = new Win(); Win win3 = new Win(); // 设置线程的名字 win1.setName("窗口一:"); win2.setName("窗口二:"); win3.setName("窗口三:"); // 开启线程 win1.start(); win2.start(); win3.start(); } } /*方式二:同步方法 * */ class Win extends Thread { private static int ticket = 100; private static Object object = new Object(); @Override public void run() { while (true) { show(); if (ticket == 0) { break;// 用于跳出循环 } } } /* 定义一个同步方法,注意必须把这个方法定义为一静态方法 * */ private static synchronized void show() { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"获取到了第"+ticket+"票"); ticket--; } } }
同步方法总结:
/** * 同步机制解决“继承Thread类”的线程安问题 * 关于同步方法的总结: * 1. 同步方法任然设计来到同步监视器,只是不需要我们显示的声明。 * 2. 非静态同步方法,同步监视器是this * 3. 静态同步方法,同步监视器是类本身 */
十一、线程的死锁问题
11.1 死锁简介
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。【如:有多把锁】
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
11.2 死锁解决方法
-
专门的算法、原则
-
尽量减少同步资源的定义
-
尽量避免嵌套同步
-
写代码时,要尽量避免死锁
十二、方式三:Lock(锁)
12.1 Lock简介
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
总结:Lock锁是java在5.0之后提出来的一种线程同步机制,我们可以把它 列为解决线程安全的第三种方式
12.2 列子
// 卖票
public class RunMainLock {
public static void main(String[] args) {
Wins wins = new Wins();
/*创建三个线程*/
Thread win1 = new Thread(wins);
Thread win2 = new Thread(wins);
Thread win3 = new Thread(wins);
/*设置线程名字*/
win1.setName("窗口一:");
win2.setName("窗口二:");
win3.setName("窗口三:");
/*开启线程*/
win1.start();
win2.start();
win3.start();
}
}
class Wins implements Runnable{
private int ticket = 100;
/*定义Lock锁*/
/*第一步:实例化ReentrantLock*/
private ReentrantLock lock = new ReentrantLock();/* 参数为true,线程先进先出;默认为false,即cpu轮到谁就谁,看运气*/
@Override
public void run() {
while (true) {
try{
/*第二步:加锁*/
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票——" + ticket);
ticket--;
} else {
break;
}
}finally {
/*第三步:解锁*/
lock.unlock();
}
}
}
}
12.3 synchronized与Lock的异同
-
相同点:
- 都能解决线程的安全问题
-
不同点
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
-
总结:
优先使用顺序(建议):
Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
十三、线程的通信问题
13.1 初步代码
让线程执行一次之后,阻塞,把cpu资源给其他线程。
/**
* 使用两个线程打印 1-100。线程1, 线程2 交替打印
*/
public class Test {
public static void main(String[] args) {
Test1 test1 = new Test1();
Thread thread1 = new Thread(test1);
Thread thread2 = new Thread(test1);
thread1.setName("线程一:");
thread2.setName("线程二:");
thread1.start();
thread2.start();
}
}
class Test1 implements Runnable{
private int j = 1;
@Override
public void run() {
while (true) {
synchronized (this) {// this的使用,指向调用该方法的对象的引用test1。在该处做为锁
if (j <= 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":打印"+j);
j++;
try {
/*第一个线程打印一个数字之后,调用wait方法便阻塞了,同时释放了锁。
第二个线程获得锁之后,打印一个数字之后,调用wait方法也阻塞了,同时释放了锁。
此时这两个线程都被阻塞了,因此只打印了2次
*/
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
结果:
总结wait方法:
从执行结果我们不难推断出,wait方法至少有2个作用或者功能:
- 阻塞当前线程
- 释放锁【不释放锁的的话,第二个线程怎么进得来】
13.2 最终代码
/**
* 使用两个线程打印 1-100。线程1, 线程2 交替打印
*/
public class Test {
public static void main(String[] args) {
Test1 test1 = new Test1();
Thread thread1 = new Thread(test1);
Thread thread2 = new Thread(test1);
thread1.setName("线程一");
thread2.setName("线程二");
thread1.start();
thread2.start();
}
}
class Test1 implements Runnable{
private int j = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
/*程序开始:假设此时第一个线程拿着锁(this)进来了,第二个线程在外面等待着。
第一个线程执行notify()方法没有什么影响,接着进入判断,打印了“线程一:打印1”后,调用wait()方法后阻塞,
同时释放锁(this)【这步操作很重要】。此时第二个线程拿到了锁进来了,执行notify()方法,唤醒第一个线程
【注意此时此刻,第二个线程还拿着锁,所以第一个线程被唤醒之后,也只能在外面等待】,接着第二个线程进入判断,
打印“线程二:打印2”后,调用wait()方法后阻塞,同时释放锁。此时此刻第一个线程又拿到了锁,接着执行。
【后面就是重复的了,第一个线程和第二个线程之间,不停的被阻塞、又不停的被唤醒,直到循环结束】
*
* */
//第二步
notify();
if (j <= 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":打印"+j);
j++;
try {
/*第一个线程打印一个数字之后,调用wait方法便阻塞了,同时释放了锁。
第二个线程获得锁之后,打印一个数字之后,调用wait方法也阻塞了,同时释放了锁。
此时这两个线程都被阻塞了,因此只打印了2次
*/
//第一步
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
结果:实现了交替打印
总结 notify():
- 唤醒一个线程【因为还有notifyAll()方法】
13.3 线程通信总结
线程通信涉及到的三个方法:
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
- notify():一旦执行此方法,就会唤醒被wait方法的一个线程,如果有多个线程被wait,就唤醒优先级高的那个,如果优先级相同,就随机唤醒一个
- notifyAll():一旦执行此方法,就会唤醒被wait方法的所有线程,
注意:
-
wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。【lock中不能使用】
-
wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则会出现IllegalMonitorStateException异常
-
wait(),notify(),notifyAll()三个方法是定义在java.lang.object类中。
为什么是定义在Object类中?
- 因为你要保证“2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。”
- 我们说过总结过任何一个对象都可以充当同步监视器,所有的对象都可以调用这三个方法,那么这三个方法必然是是定义在object类中的,因为所有的类都继承Object类。【注意Java中是单继承,但是可以多层继承。即如:Object---Parent---Child】