记录自己在狂神说java中的学习情况,文章里有自己学习的理解和扩展,新手难免有理解偏差或者错误,恳请大佬指正。
Java 多线程详解及实战小例子
线程简介
现实中有特别多同时做多件事情的例子,比如上厕所是玩手机、吃饭的时候玩手机、边听音乐边跳舞。但是其实这些任务看起来是多个任务都在做,但是本质上大脑在同一时间依旧只做了一件事情。
原来是一条路,慢慢因为车太多了,道路堵塞,效率极低。为了提高使用效率,能够充分利用道路,于是加了多个车道。
普通方法调用和多线程
普通的方法调用是跳转到方法里执行方法,然后再返回调用的地方继续往下执行。相当于一个人在做饭—》送餐—》做饭。
多线程就是再另开一个线程去执行方法。每个线程都执行一部分工作,相当于一个人做饭,一个人送餐。
程序、进程、线程
程序是存放在磁盘中的可执行文件。进程就是程序的执行实例,通俗来说,在操作系统中执行着的程序,就是进程。
一个进程中可以有多个线程,使得可以同时执行一些任务。比如你在播放音乐的时候,能同时听到音乐和歌词以及对应的MV画面。
进程(Process)与线程(Thread)
说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
而进程则是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的单位。
进程是系统分配资源的单位,里面真正执行程序的是进程里的线程(即用CPU去处理程序的语句)。
注意点:很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核CPU。如果是模拟出来的多线程,即只在一个CPU的情况下,在同一时间点,CPU只能执行一个代码,但是因为切换的很快,所以就有了同时执行的错觉。但是事实上我们平时的任务数要远大于CPU核心数,这个时候的多线程都是模拟出来的。
并发:任务数大于CPU核心数,每个任务对应的线程要交替使用CPU。并发的关键是可以处理多个任务,但是多个任务并不是真正同时处理的。
并行:任务数小于CPU核心数,每个任务对应的线程可以真正意义上的同时执行。并行的关键是同时处理多个任务。
利用Erlang 之父 Joe Armstrong的图来解释,并发就是两个队伍去排队使用咖啡机,队伍1的人用了咖啡机后下一次就是队伍2的人去使用咖啡机。而并行就是两台咖啡机可以两个队伍分别使用。
小结
- 线程就是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程,gc线程
- main()称为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如CPU调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程实现
线程有三种创建方式:继承Thread类、实现Runnable接口、实现Callable接口。
继承Tread类
启动线程的步骤如下:
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
package Threads;
// 创建线程方法一:继承Thread类,重写run()方法,调用start开启线程
// 线程开启不一定立即执行!由CPU调度执行。
public class ThreadExtendsThread extends Thread {
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 5; i++) System.out.println("我在看代码---" + i);
}
public static void main(String[] args) throws InterruptedException {
//main线程,主线程
//创建一个线程对象
ThreadExtendsThread tet = new ThreadExtendsThread();
// 调用start()方法开启线程
tet.start();
for (int i = 0; i < 5; i++)
System.out.println("我在学习多线程--" + i);
/*
我在学习多线程--0
我在看代码---0
我在学习多线程--1
我在学习多线程--2
我在看代码---1
我在看代码---2
我在看代码---3
我在学习多线程--3
我在看代码---4
我在学习多线程--4
我在看代码---5
我在看代码---6
我在学习多线程--5
我在看代码---7
我在学习多线程--6
我在看代码---8
我在学习多线程--7
我在看代码---9
我在看代码---10
我在学习多线程--8
我在看代码---11
我在学习多线程--9
我在看代码---12
我在学习多线程--10
我在看代码---13
我在看代码---14
我在看代码---15
我在学习多线程--11
我在看代码---16
我在学习多线程--12
我在看代码---17
我在学习多线程--13
我在学习多线程--14
我在看代码---18
我在学习多线程--15
我在看代码---19
我在学习多线程--16
我在学习多线程--17
我在学习多线程--18
我在学习多线程--19
* */
//可以看到start()方法会让线程同时运行,运行结果是随机的,每次运行的结果不一定一样,因为线程的调度是CPU分配的。
//但是如果用run()方法
Thread.sleep(2000);//延时2秒等待上面的线程执行完,再开始下一个实验
System.out.println("-------------------------------------------------------");
ThreadExtendsThread tet2 = new ThreadExtendsThread();
tet2.run();//start才开启了线程,而run没有,run只是线程的执行体
for (int i = 0; i < 5; i++)
System.out.println("我在学习多线程--" + i);
//因为run没有开启线程,所以还是按顺序执行
/*
我在看代码---0
我在看代码---1
我在看代码---2
我在看代码---3
我在看代码---4
我在学习多线程--0
我在学习多线程--1
我在学习多线程--2
我在学习多线程--3
我在学习多线程--4
*/
}
}
继承Thread实战案例:下载图片
首先要下载jar包来获得FileUtils类。
下载地址:http://commons.apache.org/proper/commons-io/download_io.cgi
将其解压,然后把commons-io-2.8.0.jar这个包导入到项目中去。具体的导入方法有两种:
第一种为IDEA导入:
然后找到你的jar包的路径,然后添加进去。打钩就好了。
然后就是在代码中import了,import org.apache.commons.io.FileUtils;
第二种为:
在项目中创建一个lib文件夹,然后将jar包存入lib文件夹中
然后右键lib文件夹
点击OK,然后就可以直接import了,import org.apache.commons.io.FileUtils;
下载器代码实现:
package Threads;
// Commons IO是针对开发IO流功能的工具类库
// FileUtils文件工具,复制url到文件
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
//练习Thread,实现多线程同步下载图片
public class PictureDownload extends Thread {
private String url;//网络图片地址
private String name;//保存的文件名
public PictureDownload(String url, String name) {
this.url = url;
this.name = name;
}
//下载图片线程的执行体
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载了文件名为:" + name);
}
public static void main(String[] args) {
PictureDownload pd = new PictureDownload("https://pic1.zhimg.com/80/v2-674f0d37fca4fac1bd2df28a2b78e633_720w.jpg?source=1940ef5c", "图片1.jpg");
PictureDownload pd2 = new PictureDownload("https://pic1.zhimg.com/80/2886e9751c3df175ecdfc423dfe18493_720w.jpg?source=1940ef5c", "图片2.jpg");
PictureDownload pd3 = new PictureDownload("https://pic2.zhimg.com/80/4e509eaa6a6445c87af5ac335abbb090_720w.jpg?source=1940ef5c", "图片3.jpg");
pd.start();
pd2.start();
pd3.start();
}
}
//下载器
class WebDownloader {
//下载方法
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));//把url变成文件,文件名为name的值
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO,异常,downloader方法出现问题");
}
}
}
运行结果为:(每次运行可能不一样,说明线程是同时执行的)
下载了文件名为:图片3.jpg
下载了文件名为:图片2.jpg
下载了文件名为:图片1.jpg
实现Runnable接口(十分推荐,因为Java是单继承,继承了Thread就没办法继承别的了)
启动线程的步骤如下:
- 定义MyRunnable类实现Runnable接口
- 实现run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
package Threads;
//创建线程方法2:实现runnable接口,重写run方法,执行线程需要丢入runnable接口类,调用start方法
public class ThreadRunable implements Runnable {
public void run() {
// run方法线程体
for (int i = 0; i < 10; i++) {
System.out.println("我在看代码--" + i);
}
}
public static void main(String[] args) {
//创建runnable接口的实现类对象
ThreadRunable tr = new ThreadRunable();
//创建线程对象,通过线程对象来开启我们的线程,代理,这里先不用知道代理是什么,就理解为你给我东西,我帮你执行就好了
//Thread thread = new Thread(tr);
//thread.start();
//可以用匿名对象简化为
new Thread(tr).start();
for (int i = 0; i < 10; i++) {
System.out.println("我在学习多线程--" + i);
}
/*
我在学习多线程--0
我在看代码--0
我在学习多线程--1
我在学习多线程--2
我在看代码--1
我在学习多线程--3
我在看代码--2
我在学习多线程--4
我在看代码--3
我在学习多线程--5
我在看代码--4
我在学习多线程--6
我在看代码--5
我在学习多线程--7
我在看代码--6
我在学习多线程--8
我在看代码--7
我在学习多线程--9
我在看代码--8
我在看代码--9
*/
}
}
实现Runnable类与继承Thread类实现多线程的对比
继承Thread类
- 子类继承Thread类具备多线程能力
- 启动线程:子类对象.start()
- 不建议使用,因为Java中单继承有局限性,继承了Thread类就不好继承别的类了
实现Runnalbe接口
- 实现接口Runnable具有多线程能力
- 启动线程:传入目标对象+Thread对象.start()
- 推荐使用,避免了单继承局限性,灵活方便,方便同一个对象被多个线程使用
利用实现Runnable接口让一个对象被多个线程使用
package Threads;
import com.hbq.collectionsuse.TreeMapUse;
//多个线程同时操作一个对象
//买火车票
//发现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱,像这个例子,小明先拿了第二张票,小红才拿第三张
public class ManyThread implements Runnable {
//有几张票
private int ticketNum = 10;
public void run() {
while (true) {
if (ticketNum <= 0) break;
//Thread.currentThread().getName()拿到当前线程的名字
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "张票");
}
}
public static void main(String[] args) {
ManyThread mt = new ManyThread();
// 传入Thread时可以给线程起名字
new Thread(mt, "小明").start();
new Thread(mt, "小红").start();
new Thread(mt, "黄牛").start();
/*
小红拿到了第10张票
小明拿到了第9张票
黄牛拿到了第8张票
小明拿到了第6张票
小红拿到了第7张票
小明拿到了第4张票
黄牛拿到了第5张票
小明拿到了第2张票 这里就发现问题了,小明先拿了第2张,然后小红才拿了第三张
小红拿到了第3张票
黄牛拿到了第1张票
*/
}
}
实现Runnable类实战:龟兔赛跑
package Threads;
public class TurtleAndRabbit implements Runnable {
private static String winner;
public void run() {
for (int i = 0; i < 100; i++) {
//模拟兔子睡觉
if (Thread.currentThread().getName().equals("兔子")){
try {
Thread.sleep(1);//睡1毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
i+=10;//睡醒后一下子跑十步
}
//判断比赛是否结束
boolean flag = gameOver(i);
if (flag) break;//比赛结束,停止程序
System.out.println(Thread.currentThread().getName() + "跑了" + i + "步");
}
}
//判断是否有胜利者,即游戏是否结束
private boolean gameOver(int step) {
if (winner != null) { //先判断是否有胜利者,如果有就不需要再判断了
return true;
}
if (step == 99) {//如果之前没有胜利者,而且现在这个线程到了终点,则这个线程就是胜利者
winner = Thread.currentThread().getName();
System.out.println(winner + "获胜了");
return true;
}
return false;
}
public static void main(String[] args) {
TurtleAndRabbit tr = new TurtleAndRabbit();//同一个对象(赛道)
new Thread(tr, "兔子").start();
new Thread(tr, "乌龟").start();
}
}
实现Callable接口
启动线程的步骤如下:
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行:Future result1 = ser.submit(t1);
- 获取结果:boolean r1 = result1.get();
- 关闭服务:ser.shutdownNow();
改写图片下载器
package Threads;
// Commons IO是针对开发IO流功能的工具类库
// FileUtils文件工具,复制url到文件
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
// 线程创建方式三:实现callable接口
/*
callable的好处
1.可以定义返回值
2.可以抛出异常
*/
public class PictureDownloadCallable implements Callable<Boolean> {
private String url;//网络图片地址
private String name;//保存的文件名
public PictureDownloadCallable(String url, String name) {
this.url = url;
this.name = name;
}
//下载图片线程的执行体
public Boolean call() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载了文件名为:" + name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
PictureDownloadCallable pd = new PictureDownloadCallable("https://pic1.zhimg.com/80/v2-674f0d37fca4fac1bd2df28a2b78e633_720w.jpg?source=1940ef5c", "图片1.jpg");
PictureDownloadCallable pd2 = new PictureDownloadCallable("https://pic1.zhimg.com/80/2886e9751c3df175ecdfc423dfe18493_720w.jpg?source=1940ef5c", "图片2.jpg");
PictureDownloadCallable pd3 = new PictureDownloadCallable("https://pic2.zhimg.com/80/4e509eaa6a6445c87af5ac335abbb090_720w.jpg?source=1940ef5c", "图片3.jpg");
//创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> r1 = ser.submit(pd);
Future<Boolean> r2 = ser.submit(pd2);
Future<Boolean> r3 = ser.submit(pd3);
//获取结果
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();
//关闭服务
ser.shutdownNow();
}
//下载器
class WebDownloader {
//下载方法
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));//把url变成文件,文件名为name的值
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO,异常,downloader方法出现问题");
}
}
}
}
线程状态
- 最开始是线程创建阶段,也就是在Thread t = new Thread()语句执行以后,线程对象就进入了创建状态。转2。
- 然后调用start()方法,线程进入就绪状态。转3。
- 如果此时CPU调用了该线程,那么该线程就进入运行状态。开始真正执行线程体的代码块。如果中途有阻塞,则到4,否则到6。
- 当调用了sleep、wait或者锁定的时候,线程就进入了阻塞状态。此时等待阻塞结束。转5。
- 阻塞结束后,会进入到就绪状态,返回3。
- 线程中断或者结束,线程进入死亡状态,一旦进入了死亡状态,就不能再次启动。
线程的一些方法:
方法 | 说明 |
---|---|
setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long milis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt() | 中断线程,已经不太被使用了 |
boolean isAlive() | 测试线程是否处于活动状态(就绪状态或运行状态) |
如何让线程停止?
- 不推荐使用JDK提供的stop()、destroy()方法。
- 推荐让线程自己停止下来
- 建议使用一个标志位进行终止变量,比如当flag==false的时候,则终止线程的运行。
package Threads;
// 1.建议线程正常停止-->尽量利用次数,不建议死循环,如果非要死循环,则可以加点时延,避免cpu卡死
// 2.建议使用标志位-->设置一个标志位,在死循环里利用标志位停止线程
// 3.不要使用官方自带的stop或者destroy等过时或者JDK不建议使用的方法停止线程
public class StopThread implements Runnable {
// 1.设置一个标志位
private boolean flag = true; //标志位
@Override
public void run() {
while (flag) {
System.out.println("线程执行");
}
}
//2.设置一个公开的方法停止线程,转换标志位
public void stop() { //调用该方法就可以让标志位为false,然后就可以停止线程
this.flag = false;
}
}
线程休眠(阻塞,sleep)
- sleep(时间)指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException
- sleep时间到达后线程进入就绪状态
- sleep可以模拟网络延时,倒计时等
- 每一个对象都有一个锁,sleep不会释放锁
使用方法:
try{
Thread.sleep(要休眠的毫秒数);
} catch(InterruptedException e) {
e.printStackTrace();
}
实例:
package Threads;
import java.text.SimpleDateFormat;
import java.util.Date;
// 模拟网络延时:放大问题的发生性,要不然代码是发现不了问题的
// 模拟倒计时:
public class ThreadSleep implements Runnable {
//有几张票
private int ticketNum = 10;
public void run() {
while (true) {
if (ticketNum <= 0) break;
// 模拟网络延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "张票");
}
}
//模拟倒计时
public void tenDown() throws InterruptedException {
int num = 10;
while (num >= 0) {
Thread.sleep(1000);//每一秒输出一个数字
System.out.println(num--);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadSleep ts = new ThreadSleep();
new Thread(ts, "小明").start();
new Thread(ts, "小红").start();
new Thread(ts, "黄牛").start();
ts.tenDown();
//打印当前系统时间
Date startTime = new Date(System.currentTimeMillis());
while (true){
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis());
}
}
}
线程礼让(yield)
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,礼让不一定成功!看CPU心情
使用方法:
Thread.yield();
实例:
package Threads;
//测试礼让线程
//礼让不一定成功,看CPU心情
public class ThreadYield {
public static void main(String[] args) {
MyYield my = new MyYield();
new Thread(my,"线程1").start();
new Thread(my,"线程2").start();
/*
线程1线程开始执行
线程2线程开始执行 礼让成功,会让线程2去跑
线程1线程停止执行
线程2线程停止执行
*/
}
}
class MyYield implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行");
Thread.yield();//礼让
System.out.println(Thread.currentThread().getName() + "线程停止执行");
}
}
线程的强制运行(join)
- join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
- 可以理解成插队
比如线程A中有线程B的join指令,则线程A要等到线程B执行完之后,才能执行join指令下面的操作。
使用方法:
try {
thread.join();//插队
} catch (InterruptedException e) {
e.printStackTrace();
}
实例:
package Threads;
//测试join方法
public class ThreadJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程vip插队" + i);
}
}
public static void main(String[] args) {
// 启动线程
ThreadJoin tj = new ThreadJoin();
Thread thread = new Thread(tj);
thread.start();
// 主线程
for (int i=0;i<100;i++){
System.out.println("main线程"+i);
if (i==20){ //从主线程到20之前,都是两个线程一起执行,数到20后,主线程要等tj执行完了才继续执行
try {
thread.join();//插队
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
线程的状态观测
Thread.State
线程状态。线程可以处于以下状态之一:
- New:尚未启动的线程处于此状态
- RUNNABLE:在Java虚拟机中执行的线程处于此状态
- BLOCKED:被阻塞等待监视器锁定的线程处于此状态
- WAITING:正在等待另一个线程执行特定动作的线程处于此状态
- TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- TERMINATED:已退出的线程处于此状态
一个线程可以在给定时间点处于一个状态,这些状态是不反映任何操作系统线程状态的虚拟机状态。
package Threads;
//观察测试线程的状态
public class ThreadState {
public static void main(String[] args) {
// 这里使用了lambda实现了Thread里的Runnable接口,因为Runnable接口只有一个抽象方法run,所以满足函数式接口的条件,可以使用lambda方法简化成()->{//run里的线程体具体实现}
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);//会导致线程进入TIMED_WAITING状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("..............");
});
// 观察状态
Thread.State state = thread.getState();
System.out.println(state); //NEW
//观察启动后
thread.start();
state = thread.getState();
System.out.println(state); //RUNNABLE
while (state != Thread.State.TERMINATED) {//只要线程不死,就一直输出状态
try {
Thread.sleep(500);
state = thread.getState();//更新线程状态
System.out.println(state);//输出状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//thread.start();// 会报错,死亡后的线程不能再启动了。Exception in thread "main" java.lang.IllegalThreadStateException
}
}
线程的优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程的优先级用数字表示,范围从1~10
- Thread.MIN_PRIORITY=1
- Thread.MAX_PRIORITY=10
- Thread.NORM_PRIORITY=5
使用getPriority()来获取优先级,使用setPriority(int xxx)来改变优先级
注意!线程优先级高不一定先执行,只是更有可能先执行而已。线程执行是看CPU决定的。
实例:
package Threads;
public class ThreadPriority implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
}
public static void main(String[] args) {
//主线程默认优先级
System.out.println(Thread.currentThread().getPriority());
ThreadPriority tp = new ThreadPriority();
Thread t1 = new Thread(tp);
Thread t2 = new Thread(tp);
Thread t3 = new Thread(tp);
Thread t4 = new Thread(tp);
Thread t5 = new Thread(tp);
Thread t6 = new Thread(tp);
//先设置优先级,再启动
t1.start();
t2.setPriority(1);
t2.start();
t3.setPriority(4);
t3.start();
t4.setPriority(10);
t4.start();
/*
5
Thread-0-->5
Thread-1-->1
Thread-3-->10
Thread-2-->4
*/
// t5.setPriority(-1); //Exception in thread "main" java.lang.IllegalArgumentException
// t5.start();
//
// t6.setPriority(11); //Exception in thread "main" java.lang.IllegalArgumentException
// t6.start();
t5.setPriority(8);
t5.start();
t6.setPriority(7);
t6.start();
/*
5
Thread-0-->5
Thread-1-->1
Thread-2-->4
Thread-3-->10 这回优先级高的也不一定先执行了
Thread-4-->8
Thread-5-->7
*/
}
}
有可能出现性能倒置问题:也就是优先级低的运行了,优先级高的在那等待。
守护线程(daemon)
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如,后台记录操作日志、监控内存、垃圾回收等待
注意线程的守护线程是守护用户线程的线程,只要用户线程一死,守护线程也得陪葬。可以理解为:用户线程是皇帝,守护线程是皇帝的爱臣,皇帝一死,爱臣陪葬!
-
thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
-
在Daemon线程中产生的新线程也是Daemon的。
-
守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
实例:
package Threads;
// 测试守护线程
// 上帝守护你
public class ThreadDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
thread.setDaemon(true);//默认是false表示是用户线程,正常的都是用户线程
thread.start();//上帝守护线程启动
new Thread(you).start(); //用户线程启动
//可以看到理论上这个上帝线程while(true)是不会停止的,但是设置了守护线程以后,会跟着用户线程一起陪葬
}
}
//上帝
class God implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("上帝保佑着你");
}
}
}
// 你
class You implements Runnable {
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("开心的活着");
}
System.out.println("开心地嗝屁了,goodbye,world");
}
}
线程同步(*)
现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如食堂排队打饭,每个人都想吃饭,最天然的解决方法就是,排队,一个个来。
处理多线程问题时,多个线程访问同一个对象(并发问题),并且某些线程还想修改这个对象,这个时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
队列和锁
进厕所排队形成队列,但是如果厕所门开着,还是不安全,万一有人冲进来呢,所以还需要锁门。我们需要队列和锁来解决线程不安全的问题,这就叫线程的同步。
线程的同步
由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他需要这个资源的线程就必须等待,使用后再释放锁。
但是这样可能会存在以下的问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
线程不安全的三大案例
案例一:买票
package Threads.syn;
//不安全的买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket bt=new BuyTicket();
new Thread(bt,"李四").start();
new Thread(bt,"张三").start();
new Thread(bt,"王麻子").start();
/*
线程不安全
张三拿到了id为9的票
王麻子拿到了id为10的票
李四拿到了id为8的票
李四拿到了id为7的票
张三拿到了id为6的票
王麻子拿到了id为7的票
王麻子拿到了id为5的票
李四拿到了id为3的票
张三拿到了id为4的票
张三拿到了id为2的票 买了重复的id的票
李四拿到了id为2的票
王麻子拿到了id为1的票
因为没有让他们排队买票,他们都看到系统中还有id为2的票,所以都买了,这就导致了重复买
*/
}
}
class BuyTicket implements Runnable {
//票
private int ticketNums = 10;
boolean flag = true;
@Override
public void run() {
//买票
while (flag) {
buy();
}
}
public void buy() {
//判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
// 模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//买票
System.out.println(Thread.currentThread().getName() + "拿到了id为" + ticketNums--+"的票");
}
}
案例二:银行取钱
package Threads.syn;
import com.hbq.oop.Polymorphism.A;
//不安全的取钱
//两个人去银行取同一个账户的钱
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
Drawing you = new Drawing(account, 50, "张三", 0);
Drawing wife = new Drawing(account, 100, "李四", 0);
you.start();
wife.start();
/*
结婚基金余额为:50
结婚基金余额为:50
张三手里的钱:50
李四手里的钱:100 无中生钱
*/
}
}
//账户
class Account {
int money;//账户余额
String card;//卡名
public Account(int money, String card) {
this.money = money;
this.card = card;
}
}
//银行:模拟取款
class Drawing extends Thread {
Account account;//账户
int drawingMoney;//取了多少钱
int curmoney;//现在手里有多少钱
public Drawing(Account account, int drawingMoney, String name, int curmoney) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
this.curmoney = curmoney;
}
//取钱
public void run() {
// 判断钱够不够
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
return;
}
// 模拟延时,sleep可以放大问题的发生性,如果现实中这里发生了时延,就会出问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卡内余额=余额-你取的钱
account.money = account.money - drawingMoney;
// 手里的钱
curmoney = curmoney + drawingMoney;
System.out.println(account.card + "余额为:" + account.money);
// Thread.currentThread().getName() 等价于 this.getName(),因为我们是继承Thread类的,有Thread类的getName方法
System.out.println(this.getName() + "手里的钱:" + curmoney);
}
}
案例三:列表不安全
package Threads.syn;
import java.util.ArrayList;
import java.util.List;
//线程不安全的集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
},"Thread-"+i).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
//输出9999,而不是10000,是不安全的
}
}
出现这些问题的根本原因还是因为在判断能否进行操作的时候,看到的是可以,但是期间被另一个线程修改了对象的数据,导致不满足进行操作的条件,但是此时每个线程还以为可以操作。
同步方法
- 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制来解决同步问题,这套机制就是synchronized关键字,它包含synchronized方法和synchronized块。
//同步方法
public synchronized void method(int args){}
- synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会被阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
- 缺陷:如果将一个大的方法声明为synchronized将会影响效率。
将之前的买票案例的buy方法中增加synchronized关键字,解决同步问题。
package Threads.syn;
//安全的买票
public class SafeBuyTicketSynMethod {
public static void main(String[] args) {
BuyTicket2 bt = new BuyTicket2();
new Thread(bt, "李四").start();
new Thread(bt, "张三").start();
new Thread(bt, "王麻子").start();
/*
李四拿到了id为10的票
王麻子拿到了id为9的票
张三拿到了id为8的票
王麻子拿到了id为7的票
李四拿到了id为6的票
王麻子拿到了id为5的票
张三拿到了id为4的票
王麻子拿到了id为3的票
李四拿到了id为2的票
王麻子拿到了id为1的票
*/
}
}
class BuyTicket2 implements Runnable {
//票
private int ticketNums = 10;
boolean flag = true;
@Override
public void run() {
//买票
while (flag) {
buy();
// 模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//增加synchronized同步方法,锁的是this
private synchronized void buy() {
//判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
// 模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//买票
System.out.println(Thread.currentThread().getName() + "拿到了id为" + ticketNums-- + "的票");
}
}
但是这种同步方法默认锁的是this,如果放在银行取钱的案例中,synchronized锁的对象应该是Account,但是将run方法中使用同步方法,锁的是Drawing这个对象,所以不能解决多人取同一个账户的钱的不安全问题。
其中you和wife是两个对象,synchronized同步方法锁run的话只会锁住当前实例对象,也就是分别锁住了you和wife,锁的对象都不同,就没办法实现同步。
同步方法弊端
方法里面需要修改的内容才需要锁,锁的太多很浪费资源。所以更建议使用同步块。
同步块
-
同步块:synchronized(Obj){}
-
Obj称之为同步监视器
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,就是这个对象本身,或者是class
-
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
将之前的银行取钱案例取钱代码放到synchronized同步块里锁住account对象,让多个线程不能同时操作这同一个account对象。
package Threads.syn;
//安全的取钱
//两个人去银行取同一个账户的钱
public class safeBankSyn {
public static void main(String[] args) {
Account2 account = new Account2(100, "结婚基金");
Drawing2 you = new Drawing2(account, 50, "张三", 0);
Drawing2 wife = new Drawing2(account, 100, "李四", 0);
you.start();
wife.start();
/*
结婚基金余额为:50
张三手里的钱:50
李四钱不够,取不了
*/
}
}
//账户
class Account2 {
int money;//账户余额
String card;//卡名
public Account2(int money, String card) {
this.money = money;
this.card = card;
}
}
//银行:模拟取款
class Drawing2 extends Thread {
Account2 account;//账户
int drawingMoney;//取了多少钱
int curmoney;//现在手里有多少钱
public Drawing2(Account2 account, int drawingMoney, String name, int curmoney) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
this.curmoney = curmoney;
}
//取钱
public void run() {
// 锁的对象是要变化的量,需要增删改查的对象
synchronized (account) { //锁住account这个对象,让不同的线程不能同时操作这个对象
// 判断钱够不够
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
return;
}
// 模拟延时,sleep可以放大问题的发生性
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卡内余额=余额-你取的钱
account.money = account.money - drawingMoney;
// 手里的钱
curmoney = curmoney + drawingMoney;
System.out.println(account.card + "余额为:" + account.money);
// Thread.currentThread().getName() 等价于 this.getName(),因为我们是继承Thread类的,有Thread类的getName方法
System.out.println(this.getName() + "手里的钱:" + curmoney);
}
}
}
同理,不安全的集合,在list添加的时候锁住list对象,也能实现线程同步。
package Threads.syn;
import java.util.ArrayList;
import java.util.List;
//线程安全的集合
public class safeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
synchronized (list){ //加锁就安全了
list.add(Thread.currentThread().getName());
}
},"Thread-"+i).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
//10000,是安全的
}
}
小结:
- 对于普通同步方法,锁是当前实例对象。 如果有多个实例,那么锁对象必然不同无法实现同步。
- 对于静态同步方法,锁是当前类的Class对象。有多个实例,但是锁对象是相同的 ,可以完成同步。
- 对于同步方法块,锁是Synchonized括号里配置的对象。对象最好是只有一个的,如当前类的class是只有一个的,锁的对象相同也能实现同步。
扩充内容CopyOnWriteArrayList
这个类本身就是线程安全的list,可以直接使用。
package Threads.syn;
import java.util.concurrent.CopyOnWriteArrayList;
// 测试JUC安全类型的集合
public class JUC {
public static void main(String[] args) {
CopyOnWriteArrayList<String> cowal = new CopyOnWriteArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
cowal.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(cowal.size());
}
}
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
死锁实例:
package Threads.syn;
// 死锁:多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLock {
public static void main(String[] args) {
MakeUp girl1 = new MakeUp(0, "灰姑娘");
MakeUp girl2 = new MakeUp(1, "白雪公主");
girl1.start();
girl2.start();
}
}
//口红
class LipStick {}
//镜子
class Mirror {}
class MakeUp extends Thread {
// 需要的资源只有一份,可以使用static
static LipStick lipStick = new LipStick();
static Mirror mirror = new Mirror();
int choice;//选择
String girlName;
public MakeUp(int choice, String girlName) {
super(girlName);
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
super.run();
// makeup();
makeup2();
}
//化妆。互相持有对方的锁,就是需要拿到对方的资源,这种方式会导致死锁
private void makeup() {
if (choice == 0) {
synchronized (lipStick) {//获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mirror) {//一秒后想获得镜子
System.out.println(this.girlName + "获得镜子的锁");
}
}
} else {
synchronized (mirror) {//获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lipStick) {//一秒后想获得口红
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
//化妆。修改后破坏死锁的条件,使得不死锁
private void makeup2() {
if (choice == 0) {
synchronized (lipStick) {//获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//破坏请求与保持条件,上面的口红用完后就把口红锁释放,这样另一个人就可以用口红了,然后再用镜子。
synchronized (mirror) {//一秒后想获得镜子
System.out.println(this.girlName + "获得镜子的锁");
}
} else {
synchronized (mirror) {//获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (lipStick) {//一秒后想获得口红
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
产生死锁的四个必要条件
- 互斥条件:一个资源一次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不可剥夺条件:进程获得的资源,在未使用完之前,不能被强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
上述条件只要破坏其中一个以上,就可以避免死锁的发生。
锁(Lock)
从jdk5开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象。
Java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
实例:
package Threads.syn;
import java.util.concurrent.locks.ReentrantLock;
//测试Lock锁
public class LockUse {
public static void main(String[] args) {
Lock2 l2 = new Lock2();
new Thread(l2).start();
new Thread(l2).start();
new Thread(l2).start();
}
}
class Lock2 implements Runnable {
int ticetNums = 10;
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//加锁
lock.lock();
if (ticetNums > 0) {
System.out.println(Thread.currentThread().getName()+"获得了:"+ticetNums--);
} else {
break;
}
} finally {//建议放在finally里保证要解锁
//解锁
lock.unlock();
}
try { //抢完票给个时延,避免黄牛疯抢
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
synchronized和Lock的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放锁。
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(方法体之外)
线程通信
线程之间有的时候也需要互相传递数据,互相进行通信。
应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,又需要马上通知消费者消费
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。
- 在生产者消费者问题中,仅有synchronized是不够的
- synchronized可阻止并发更新同一个共享资源,实现了同步
- synchronized不能用来实现不同线程之间消息传递(通信)
Java中提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的对象 |
notifyAll() | 唤醒同一个对象所有调用wait()方法的线程,优先级别高的线程优先调度 |
注意:这些方法全都是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException。
解决方法1:并发协作模型“生产者/消费者模式”——>管程法
- 生产者:负责生产数据的模块(可能是方法、对象、线程、进程)
- 消费者:负责处理数据的模块(可能是方法、对象、线程、进程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据,如果数据缓冲区没满,则生产者才可以继续生产。如果数据缓冲区有数据,则消费者才可以继续消费。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nehASKtd-1618110069947)(E:\硕士毕业计划\java学习\生产者消费者.png)]
尝试实战一个生产者消费者的例子,我们看看只使用synchronized会怎么样:
package Threads.syn;
//测试:生产者消费者模型——>利用缓冲区解决:管程法
public class PC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start(); //生产者总共生产了97个产品
new Customer(container).start(); //消费者总共消费了97个产品
}
}
//生产者
class Productor extends Thread {
SynContainer container;
int counts = 0;//记录自己生产了几个产品
public Productor(SynContainer container) {
this.container = container;
}
//生产
public void run() {
for (int i = 0; i < 100; i++) {
boolean flag = container.push(new Product(i));
if (flag) counts++;
System.out.println("生产了第" + i + "个产品");
}
System.out.println("生产者总共生产了" + counts + "个产品");
}
}
//消费者
class Customer extends Thread {
SynContainer container;
int counts = 0;//记录自己消费了几个产品
public Customer(SynContainer container) {
this.container = container;
}
public void run() {
for (int i = 0; i < 100; i++) {
Product flag = container.pop();
if (flag != null) {
System.out.println("消费了id为" + flag.id + "的产品");
counts++;
}
}
System.out.println("消费者总共消费了" + counts + "个产品");
}
}
// 产品
class Product {
int id;
public Product(int id) {
this.id = id;
}
}
//缓冲区
class SynContainer {
Product[] products = new Product[10];
int count = 0;
//生产者放入产品
public synchronized Boolean push(Product product) {
//如果容器满了,就需要等待消费者消费
if (count == products.length) {
return false;
}
//如果没有满,就需要丢入产品
products[count++] = product;
return true;
//生产好产品了,通知消费者消费
}
//消费者消费产品
public synchronized Product pop() {
//判断能否消费
if (count == 0) {
return null;
}
count--;
Product product = products[count];
//消费完了,通知生产者生产
return product;
}
}
可以看到,这样会导致最终生产者没有生产够100个商品,只生产了97个商品(每次运行可能都不一样)。
所以仅仅使用synchronized是不够的,还必须使用wait和notify方法。
package Threads.syn;
//测试:生产者消费者模型——>利用缓冲区解决:管程法
public class PC1 {
public static void main(String[] args) {
SynContainer1 container = new SynContainer1();
new Productor1(container).start(); //生产者总共生产了100个产品
new Customer1(container).start(); //消费者总共消费了100个产品
}
}
//生产者
class Productor1 extends Thread {
SynContainer1 container;
int counts = 0;//记录自己生产了几个产品
public Productor1(SynContainer1 container) {
this.container = container;
}
//生产
public void run() {
for (int i = 0; i < 100; i++) {
boolean flag = container.push(new Product1(i));
if (flag) counts++;
System.out.println("生产了第" + i + "个产品");
}
System.out.println("生产者总共生产了" + counts + "个产品");
}
}
//消费者
class Customer1 extends Thread {
SynContainer1 container;
int counts = 0;//记录自己消费了几个产品
public Customer1(SynContainer1 container) {
this.container = container;
}
public void run() {
for (int i = 0; i < 100; i++) {
Product1 flag = container.pop();
if (flag != null) {
System.out.println("消费了id为" + flag.id + "的产品");
counts++;
}
}
System.out.println("消费者总共消费了" + counts + "个产品");
}
}
// 产品
class Product1 {
int id;
public Product1(int id) {
this.id = id;
}
}
//缓冲区
class SynContainer1 {
Product1[] products = new Product1[10];
int count = 0;
//生产者放入产品
public synchronized boolean push(Product1 product) {
//如果容器满了,就需要等待消费者消费
if (count == products.length) {
//通知消费者消费,生产等待
try {
this.wait();
} catch (InterruptedException e) {
return false;
}
}
//如果没有满,就需要丢入产品
products[count++] = product;
//生产好产品了,通知消费者消费
this.notifyAll();
return true;
}
//消费者消费产品
public synchronized Product1 pop() {
//判断能否消费
if (count == 0) {
//等待生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
return null;
}
}
count--;
Product1 product = products[count];
//消费完了,通知生产者生产
this.notifyAll();
return product;
}
}
注意:这里面的数组其实是有问题的,真正的解决方法应该自己再写一个能固定长度的队列来实现。
解决方法2:并发协作模型“生产者/消费者模式”——>信号灯法
如果信号为true则生产,否则就等待。消费者也一样。这有点像缓存区大小为1的管程法。
package Threads.syn;
//测试生产者消费者问题2:信号灯法,标志位解决
public class PC2 {
public static void main(String[] args) {
TV tv = new TV();
Player p = new Player(tv);//同一台电视播放
Audience a = new Audience(tv);//同一台电视播放
new Thread(p).start();
new Thread(a).start();
}
}
//生产者——>演员
class Player extends Thread {
TV tv;
public Player(TV tv) {
this.tv = tv;
}
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
tv.play("快乐大本营");
} else {
tv.play("广告");
}
}
}
}
//消费者——>观众
class Audience extends Thread {
TV tv;
public Audience(TV tv) {
this.tv = tv;
}
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//产品——>节目
class TV {
//演员表演,观众等待
//观众观看,演员等待
String show;//表演的节目
boolean flag = true;
//表演
public synchronized void play(String show) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + show);
//通知观众观看
this.notifyAll();
this.show = show;
this.flag = !this.flag;
}
//观看
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了" + show);
//通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
线程池
背景:经常创建和销毁线程、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。类似生活中的公共交通工具。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
JDK5起提供了线程池相关API:ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类:ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- <T>Future <T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
- void shutdown():关闭连接池
Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池
实例:
package Threads.syn;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//测试线程池
public class ThreadPool {
public static void main(String[] args) {
//1.创建服务,创建线程池
// newFixedThreadPool 参数:线程池大小
ExecutorService service = Executors.newFixedThreadPool(3);
//2.执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
/*
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
pool-1-thread-3 //重复利用线程
pool-1-thread-1
*/
//3.关闭连接
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}