目录描述
- 1.死锁概念
- 2.开发中如何解决线程安全问题
- 3.守护线程
- 3.1 概述
- 3.2 实现
- 4.定时器 Timer
- 4.1 概述
- 4.2 实现
- 5.实现线程的第三种方式 Callable
- 6.wait和notify方法
- 6.1 基础概念
- 6.2 生产者和消费者模式
- 6.3 实现生产者和消费者模式
- 6.4 编程练习题
1.死锁概念
死锁代码需要会写,一般面试官也要求你会写,只有会写,才会在以后的开发中注意这个事情,因为死锁很难调试
死锁示意图:
package deadlock;
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
//t1和t2两个线程共享o1,o2
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread{
Object o1;
Object o2;
public MyThread2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
运行结果:
2.开发中如何解决线程安全问题
- 第一种方案:尽量使用局部变量代替“实例变量和静态变量”
- 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了(一个线程对应1个对象,100个线程对应100个对象,对象不共享了,就没有数据安全问题了)
- 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized【线程同步机制】
注意:不要一上来就选择线程同步synchronized,因为synchronized会让程序的执行效率降低,系统的用户吞吐量降低,用户体验差,在不得已的情况下再选择线程同步机制
3.守护线程
3.1 概述
Java语言中线程分为两大类:用户线程和守护线程【后台线程,例如:垃圾回收器】
守护线程特点:
- 一般守护线程是一个四循环
- 所有的用户线程只要结束,守护线程自动结束
注意:主线程main方法是一个用户线程
问题:守护线程用在哪?
答:每天零点的时候系统数据自动备份【这个需要使用到定时器,并且可以将定时器设置为守护线程,一直在那里看着,每到零点的时候就备份一次,所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了】
3.2 实现
主要代码: t.setDaemon(true);
package thread;
public class ThreadTest14 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据的线程");
//启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
//主线程:主线程是用户线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread{
@Override
public void run() {
int i = 0;
//即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止
while (true){
System.out.println(Thread.currentThread().getName() + "---->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.定时器 Timer
4.1 概述
定时器作用:间隔特定的时间,执行特定的程序【在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的】
例如:每周要进行银行账户的总账操作;每天要进行数据的备份操作
在java中实现定时器方式:
- 使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务,这种方式是最原始的定时器(比较low)
- 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用,不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的
- 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务
4.2 实现
使用定时器指定定时任务
package thread;
import javax.xml.crypto.Data;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest {
public static void main(String[] args) throws Exception{
//创建定时器对象
Timer timer = new Timer();
//Timer timer = new Timer(true);
//指定定时任务
//timer.schedule(定时任务,第一次执行时间,间隔多久执行一次);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2022-04-04 11:00:00");
timer.schedule(new LogTimerTask(),firstTime,1000 * 10);
//采样匿名内部类的形式也可以
/* timer.schedule(new TimerTask(){
@Override
public void run() {
}
},firstTime,1000 * 10);*/
}
}
//编写一个定时任务类
//假设是一个记录日志的定时任务
class LogTimerTask extends TimerTask{
@Override
public void run() {
//编写你需要的执行的任务就行了
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strtTime = sdf.format(new Date());
System.out.println(strtTime + ":成功完成了一次数据备份!");
}
}
运行结果:
5.实现线程的第三种方式 Callable
实现Callable接口(JDK8新特性)
特点:这种方式实现的线程可以获取线程的返回值,之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void
优点:可以获取到线程的执行结果
缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低
package thread;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;//JUC包下的,属于java的并发包,老JDK中没有这个包,新特性
public class ThreadTest15 {
public static void main(String[] args) throws Exception{
//第一步:创建一个”未来任务类“对象
//参数非常重要,需要给一个Callable接口实现类对象
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {//call()方法就相当于run方法,只不过这个有返回值
//线程执行一个任务,执行之后可能会有一个执行结果
//模拟执行
System.out.println("call method begin!");
Thread.sleep(1000 * 10);
System.out.println("call method end!");
int a = 100;
int b = 200;
return a + b;//自动装箱(300结果变成Integer)
}
});
//创建线程对象
Thread t = new Thread(task);
//启动线程
t.start();
//这里是main方法,这是在主线程中
//在主线程中,怎么获取t线程的返回结果
//get()方法的执行会导致”当前线程阻塞“
Object obj = task.get();
System.out.println("线程执行结果:" + obj);
//main方法这里的程序要想执行必须等待get()方法的结束
//而get()方法可能需要很久,因为get()方法是为了拿另一个线程的执行结果
//另一个线程执行是需要时间的
System.out.println("hello world!");
}
}
6.wait和notify方法
6.1 基础概念
- wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的
- wait()方法作用:让正在o对象上获得的线程进入等待状态,无期限等待,直到被唤醒为止
Object o = new Object();
o.wait();//该方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态
- notify()方法作用:唤醒正在o对象上等待的线程
Object o = new Object();
o.notify();
注意:还有一个notifyAll()方法:唤醒o对象上处于等待的所有线程
6.2 生产者和消费者模式
6.3 实现生产者和消费者模式
- 使用wait和notify方法实现生产者和消费者模式
- 生产者和消费者模式:生产线程负责生产,消费线程负责消费,生产线程和消费线程要达到均衡,这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法
- wait方法和notify方法建立在线程同步的基础之上,因为多线程要同时操作一个仓库,有线程安全问题
- wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁
- notify方法作用:notify()让正在o对象上等待的线程唤醒,只是通知,不会释放掉o对象之前占有的锁
模拟:仓库使用List集合,List集合中假设只能存储1个元素,1个元素就表示仓库满了,如果List集合中元素个数是0,就表示仓库空了,保证List集合中永远都是最多存储1个元素,必须做到这种效果:生成1个消费1个
package thread;
import java.util.ArrayList;
import java.util.List;
public class ThreadTest16 {
public static void main(String[] args) {
//创建1个仓库对象,共享的
List list = new ArrayList();
//创建两个线程对象
//生产者线程
Thread t1 = new Thread(new Producer(list));
//消费者线程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
//生产线程
class Producer implements Runnable{
//仓库
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
//一直生产(使用死循环来模拟一直生产)
while (true){
//给仓库对象list加锁
synchronized (list){
if (list.size() > 0){//大于0,说明仓库中已经有1个元素了
//当前线程进入等待状态,并且释放list集合的锁
try {
//当前线程进入等待状态,并且释放Producer之前占有的list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能够执行到这里说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "-->" + obj);
//唤醒消费者消费
list.notifyAll();
}
}
}
}
//消费线程
class Consumer implements Runnable{
//仓库
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
//一直消费
while (true){
synchronized (list){
if (list.size() == 0){
try {
//仓库已经空了
//消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能够执行到此处说明仓库中有数据,进行消费
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "-->" + obj);
//唤醒生产者生产
list.notifyAll();
}
}
}
}
运行结果:
6.4 编程练习题
题目:
使用生产者和消费者模式实现,交替输出:
假设只有两个线程,输出以下结果:
t1–>1
t2–>2
t1–>3
t2–>4
t1–>5
t2–>6
…
要求:必须交替,并且t1线程负责输出奇数。t2线程负责输出偶数。
两个线程共享一个数字,每个线程执行时都要对这个数字进行:++
代码实现:
package thread;
public class exam01 {
public static void main(String[] args) {
//创建共享数字对象
Num num = new Num();
//创建两条线程
//奇数线程
Thread t1 = new Thread(new OddNum(num));
//偶数线程
Thread t2 = new Thread(new EvenNum(num));
//修改线程名称
t1.setName("t1");
t2.setName("t2");
//启动线程
t1.start();
t2.start();
}
}
//共享数字对象
class Num{
int i = 1;
}
//偶数线程
class EvenNum implements Runnable{
//共享数字
private Num num;
public EvenNum(Num num) {
this.num = num;
}
@Override
public void run() {
//死循环i++
while(true){
synchronized (num){
if (num.i % 2 == 1){
//如果num是奇数,偶数线程进入等待状态
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序进行到这说明是偶数,输出,并对数字进行++操作
System.out.println(Thread.currentThread().getName() + "--->" + (num.i++));
//一秒输出一次
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒奇数线程(?)
num.notify();
}
}
}
}
//奇数线程
class OddNum implements Runnable{
private Num num;
public OddNum(Num num) {
this.num = num;
}
@Override
public void run() {
while (true){
synchronized (num){
if (num.i % 2 == 0){
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//执行到这说明是奇数,输出,并对数字进行++操作
System.out.println(Thread.currentThread().getName() +"--->" + (num.i++));
//一秒输出一次
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒偶数线程(?)
num.notify();
}
}
}
}
运行结果: