视频链接:https://www.bilibili.com/video/BV1Rx411876f?p=1
视频范围P797 - P809
死锁代码需要会写,一般面试官也要求你会写,只有会写,才会在以后的开发中注意这个事情,因为死锁很难调试
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){
}
}
}
}
运行结果:
注意:不要一上来就选择线程同步synchronized,因为synchronized会让程序的执行效率降低,系统的用户吞吐量降低,用户体验差,在不得已的情况下再选择线程同步机制
Java语言中线程分为两大类:用户线程和守护线程【后台线程,例如:垃圾回收器】
守护线程特点:
注意:主线程main方法是一个用户线程
问题:守护线程用在哪?
答:每天零点的时候系统数据自动备份【这个需要使用到定时器,并且可以将定时器设置为守护线程,一直在那里看着,每到零点的时候就备份一次,所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了】
主要代码: 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();
}
}
}
}
定时器作用:间隔特定的时间,执行特定的程序【在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的】
例如:每周要进行银行账户的总账操作;每天要进行数据的备份操作
在java中实现定时器方式:
使用定时器指定定时任务
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 + ":成功完成了一次数据备份!");
}
}
运行结果:
实现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!");
}
}
Object o = new Object();
o.wait();//该方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态
Object o = new Object();
o.notify();
注意:还有一个notifyAll()方法:唤醒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();
}
}
}
}
运行结果:
题目:
使用生产者和消费者模式实现,交替输出:
假设只有两个线程,输出以下结果:
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();
}
}
}
}
运行结果: