Java基础关键_030_线程(三)
目 录
一、死锁
二、售票问题
1.线程安全
2.线程通信
(1)引言
(2)wait()
(3)notify()
(4)notifyAll()
(5)实例1
(6)实例2
(7)比较 wait() 和 sleep()
三、单例模式
1.饿汉式单例
(1)实现
(2)实例
2.懒汉式单例
(1)实现
(2)实例
3.懒汉式单例模式的线程安全
四、ReentrantLock(可重入锁)
1.说明
2.实例
五、实现线程的方式
1.回顾之前的两种实现方式
2.实现 Callable 接口
3.使用线程池实现
一、死锁
public class MyClassThread implements Runnable {
private Object o1;
private Object o2;
public MyClassThread(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
if ("t1".equals(Thread.currentThread().getName())) {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o2) {
System.out.println("死锁了吧");
}
}
}
if ("t2".equals(Thread.currentThread().getName())){
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o1) {
System.out.println("死锁了吧");
}
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread(new MyClassThread(o1, o2));
Thread t2 = new Thread(new MyClassThread(o1, o2));
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
程序会陷入死锁状态,即既不报错也不执行结果。所以,对于 synchronized 的使用要谨慎。
二、售票问题
1.线程安全
首先,借助售票问题回顾上一节的线程同步机制。
public class Ticket implements Runnable {
private int ticket = 50; // 总共放票50张
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket <= 0) {
System.out.println("票卖完了");
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "号售票成功!余票" + --ticket + "张");
}
}
}
}
public class Sell {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
Thread t4 = new Thread(ticket, "窗口4");
Thread t5 = new Thread(ticket, "窗口5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
2.线程通信
(1)引言
上边利用 synchronized 互斥锁虽然保证了线程安全,但是如何让多个线程顺次交替执行呢?
这就涉及到了线程通信。线程通信涉及到以下三个方法,这三个方法都是 Object 类的方法。
(2)wait()
- wait 方法有三个重载方法:
- wait():调用此方法,线程进入 等待状态;
- wait(long timeoutMillis):调用此方法,线程进入 超时等待状态;
- wait(long timeoutMillis, int nanos):调用此方法,线程进入 超时等待状态。
- 调用 wait 方法需要通过共享对象调用,而不是线程对象;
- 例如:调用 wait() 会使在共享对象上活跃的所有线程进入无期限等待,直到该共享对象调用 notify() 唤醒。唤醒后,继上一次调用 wait() 的位置继续向下执行;
- 调用该方法之后,会释放之前占用的对象锁。
(3)notify()
- 调用该方法之后,唤醒优先级最高的等待线程。若优先级相同,则随机唤醒一个;
- 该方法需要通过共享对象调用,而不是线程对象。
(4)notifyAll()
- 调用该方法之后,唤醒在共享对象上所有等待的线程;
- 该方法需要通过共享对象调用,而不是线程对象。
(5)实例1
在线程安全的售票问题代码基础上,修改 Sell.java ,示例如下:
public class Ticket implements Runnable {
private int ticket = 50; // 总共放票50张
@Override
public void run() {
while (true) {
synchronized (this) {
this.notify(); // 唤醒当前线程
if (ticket <= 0) {
System.out.println("票卖完了");
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "号售票成功!余票" + --ticket + "张");
try {
this.wait(); // 让当前线程进入无限期等待状态
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
可以看到,实现了线程交替执行的效果。
另外,需要注意的是:在 同步代码块内 或 被 synchronized 修饰的同步方法里 才能使用上述的三个方法,且 调用三个方法的共享对象 需要和 synchronized 里的共享对象一致。
(6)实例2
题目:利用线程顺次输出字母 A、B、C,循环十次。
public class LetterThreadTest {
private static Object lock = new Object();
private static boolean ist1 = true;
private static boolean ist2 = false;
private static boolean ist3 = false;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
while (!ist1) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("A");
ist1 = false;
ist2 = true;
ist3 = false;
lock.notifyAll();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
while (!ist2) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
ist1 = false;
ist2 = false;
ist3 = true;
lock.notifyAll();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
while (!ist3) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("C");
ist1 = true;
ist2 = false;
ist3 = false;
lock.notifyAll();
}
}
}
}).start();
}
}
(7)比较 wait() 和 sleep()
- wait 是 Object 的实例方法,sleep 是 Thread 的静态方法;
- wait 只能用于同步代码块或同步方法,sleep 无限制;
- wait 会释放对象锁,而 sleep 不会释放;
- wait 结束条件是凭借 notify 唤醒或到达指定时间,sleep 结束条件是到达指定时间;
- 相同点是两者都会引起阻塞。
三、单例模式
1.饿汉式单例
类加载时即完成对象创建,无论使用与否。
(1)实现
- 私有化构造方法;
- 对外提供一个公开的静态方法,以获取单个实例;
- 定义一个静态变量,类加载时初始化。
(2)实例
// 饿汉式单例模式
public class SingletonPattern {
// 1. 私有化构造方法,防止外部创建实例
private SingletonPattern() {
System.out.println("SingletonPattern constructor");
}
// 2. 创建一个私有的静态变量,用于存储唯一实例
private static SingletonPattern instance = new SingletonPattern();
// 3. 提供一个公共的静态方法,用于获取唯一实例
public static SingletonPattern getInstance() {
return instance;
}
}
public class SingletonPatternTest {
public static void main(String[] args) {
SingletonPattern instance1 = SingletonPattern.getInstance();
SingletonPattern instance2 = SingletonPattern.getInstance();
System.out.println(instance1 == instance2); // 输出:true
}
}
2.懒汉式单例
不在类加载时创建对象,需要使用时才创建。
(1)实现
- 私有化构造方法;
- 对外提供一个公开的静态方法,以获取单个实例;
- 定义一个静态变量,其值为 null 。
(2)实例
// 懒汉式单例模式
public class SingletonPattern {
// 1. 私有化构造方法
private SingletonPattern() {
System.out.println("SingletonPattern()");
}
// 2. 私有化静态变量
private static SingletonPattern instance;
// 3. 创建一个静态方法,返回一个实例
public static SingletonPattern getInstance() {
if (instance == null) {
instance = new SingletonPattern();
}
return instance;
}
public class SingletonPatternTest {
public static void main(String[] args) {
SingletonPattern instance1 = SingletonPattern.getInstance();
SingletonPattern instance2 = SingletonPattern.getInstance();
System.out.println(instance1 == instance2); // 输出:true
}
}
3.懒汉式单例模式的线程安全
public class Singleton {
private static Singleton instance = null;
private Singleton() {
System.out.println("Singleton");
}
/* 第一种方式,同步方法
public synchronized static Singleton getInstance() {
if (instance == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
instance = new Singleton();
}
return instance;
}*/
// 第二种方式,同步代码块
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) { // 获取Singleton类的类锁
if (instance == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
instance = new Singleton();
}
}
}
return instance;
}
}
public class SingletonThread {
private static Singleton s1;
private static Singleton s2;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getInstance();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
s2 = Singleton.getInstance();
}
});
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
}
四、ReentrantLock(可重入锁)
1.说明
- 使用 Lock 实现线程安全;
- Lock 接口从 jdk 5 开始引入;
- ReentrantLock 是 Lock 的一个实现类。
2.实例
对懒汉式单例模式的线程安全实例中的 Singleton.java 修改。
public class Singleton {
private static Singleton instance = null;
private static final ReentrantLock lock = new ReentrantLock(); // 创建一个可重入锁
private Singleton() {
System.out.println("Singleton");
}
public static Singleton getInstance() {
if (instance == null) {
lock.lock(); // 加锁
if (instance == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
instance = new Singleton();
}
}
lock.unlock(); // 释放锁
return instance;
}
}
注意:有 lock() ,就一定要有 unlock() 。但是比 synchronized 更灵活,且 jdk 官方更推荐使用 ReentrantLock 这种方式。
五、实现线程的方式
1.回顾之前的两种实现方式
在线程的第一节里便讨论了实现线程的两种方式:继承 Thread 类 和 实现 Runnable 接口。链接如下:
Java基础关键_028_线程(一)https://mpbeta.csdn.net/mp_blog/creation/editor/146426314
回顾之后,再来看看其他的实现方法。
2.实现 Callable 接口
这种实现方式,可以获取到线程的返回值。
public class TaskTest implements Callable<String> {
@Override
public String call() throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss SSS");
Date now = new Date();
String format = sdf.format(now);
return format;
}
}
public class ThreadTest {
public static void main(String[] args) {
TaskTest taskTest = new TaskTest();
FutureTask<String> futureTask = new FutureTask<>(taskTest);
Thread thread = new Thread(futureTask);
thread.start();
try {
String s = futureTask.get();
System.out.println(s);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
3.使用线程池实现
- 线程池本质上就是一个缓存;
- 服务器在启动时,初始化线程池。即服务器启动时,创建多个线程对象,放入线程池中,在需要时直接从线程池中获取。
public class ThreadTest {
public static void main(String[] args) {
// 创建线程池, 线程池大小为5
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 将任务提交给线程池
executorService.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
});
// 关闭线程池
executorService.shutdown();
}
}