1、多线程简介
程序是指令和数据的有序集合,其本身配如有任何运行的含义,是一个静态的概念。
进程(Process)
进程是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间,至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。操作系统调度的最小任务单位不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。
线程(Thread)
线程是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
进程和线程的关系
进程和线程是包含关系,一个进程可以包含一个或多个线程,但至少会有一个线程。多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。
多线程
Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()
方法,在main()
方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。对于大多数Java程序来说,多任务,就是如何使用多线程实现多任务。
特点
- 多线程模型是Java程序最基本的并发模型;
- 后续读写网络、数据库、Web开发等都依赖Java多线程模型。
多任务模式
同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:
- 多进程模式(每个进程只有一个线程)
- 多线程模式(一个进程有多个线程)
- 多进程+多线程模式(复杂度最高)
注意
很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即使在一个CPU下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所有就有同时执行的错觉。
2、线程的创建
2.1、三种创建方式
2.1.1、Thread:继承Thread类(重点)
不推荐使用,避免OOP单继承的局限性
package com.longxin;
//创建线程方法一:继承Thread类,重写run方法,调用start开启线程
/**
* 注意:线程开启不代表立即执行,线程的执行不受人为的控制,全权由CPU调度执行。
* */
public class TestThread extends Thread{
@Override
public void run() {
//run方法线程体
super.run();
for (int i = 1; i < 2000; i++) {
System.out.println("-------"+i);
}
}
//main方法主线程
public static void main(String[] args) {
//创建一个线程对象
TestThread testThread = new TestThread();
//调用start方法开启线程
testThread.start();
for (int i = 1; i < 2000; i++) {
System.out.println("%%%%%%%"+i);
}
}
}
run和start的区别
run:直接调用run()
方法,就是把run()
方法当做普通方法来调用,没有多线程的概念,根据程序逻辑去执行。
start:调用start()
方法,来启动线程真正实现多线程运行。
Thread下载图片练习:看下载图片的顺序是否是安装程序步骤执行的?
package com.thread.test;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
//练习thread,实现多线程同步下载图片
public class ThreadTest02 extends Thread{
//网络图片地址
private String url;
//文件名
private String name;
public ThreadTest02(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) {
ThreadTest02 th1 = new ThreadTest02
("http://pic.sc.chinaz.com/files/pic/pic9/202006/bpic20613.jpg","马吃草.jpg");
ThreadTest02 th2 = new ThreadTest02
("http://pic.sc.chinaz.com/files/pic/pic9/202006/bpic20616.jpg","方向盘.jpg");
ThreadTest02 th3 = new ThreadTest02
("http://pic.sc.chinaz.com/files/pic/pic9/202006/hpic2610.jpg","企鹅.jpg");
th1.start();
th2.start();
th3.start();
}
}
//下载器
class WebDownloader {
//下载方法
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.err.println("IO异常,downloader方法出现问题");
}
}
}
2.1.2、Runnable接口:实现Runnable接口(重点、推荐)
实现runnable接口
package com.thread.test;
/**
* 创建线程2:实现runnable接口,重写run方法,执行线程需要在Thread构造器中写入runnable接口实现类,调用start方法
* */
public class RunnableTest01 implements Runnable{
@Override
//实现Runnable接口的run方法
public void run() {
System.out.println("用Runnable建立的新线程");
}
public static void main(String[] args) {
//创建runnable接口实现类的对象
RunnableTest01 runnable = new RunnableTest01();
//通过线程对象来开启线程代理
new Thread(runnable).start();
}
}
优点:
- 避免单继承的局限性
- 灵活方便
- 方便同一个对象被多个线程使用
2.1.3、Callable接口:实现Callable接口(了解)
实现Callable接口
/*测试Callable*/
public class PoolTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//可以使用很多实现类来操作Callable,举例使用FutureTask
FutureTask<Integer> futureTask = new FutureTask(new MeThread());
//启动线程
new Thread(futureTask).start();
//可接收返回值,一定要在线程执行后获取返回值,否则程序会卡死
Integer result = futureTask.get();
System.out.println(result);
}
}
class MeThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1+1;
}
}
优点:
- 可以设置返回值
- 可以抛出异常
使用多线程留下的问题:
多个线程操作同一个资源,线程不安全,数据紊乱。
3、代理模式
3.1、静态代理
代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。
package com.thread.test3;
public class StaticProxy2 {
public static void main(String[] args) {
/*观众看电影来电影院*/
/*Cinema cinema = new Cinema(new PlayMovie());
cinema.playMovie();*/
new Cinema(new PlayMovie()).playMovie();
}
}
/*电影*/
interface Movie{
/*播放电影*/
void playMovie();
}
/*电影公司要让这个电影播放*/
class PlayMovie implements Movie{
@Override
public void playMovie() {
System.out.println("播放电影《阿甘正传》");
}
}
/*电影公司就去让电影院去代理播放任务
* 电影院为了多赚钱,就在电影片头和片尾增加了广告
* */
class Cinema implements Movie{
private Movie movie;
public Cinema(Movie movie){
this.movie = movie;
}
@Override
public void playMovie() {
before();
this.movie.playMovie();
after();
}
private void before() {
System.out.println("片头广告");
}
private void after() {
System.out.println("片尾广告");
}
}
输出结果
片头广告
播放电影《阿甘正传》
片尾广告
4、JDK1.8 Lambda表达式
为什么要用Lambda表达式?
- 避免内部类定义过多
- 让代码看起来更整洁
- 去掉一堆没有意义的代码,只留下核心逻辑
Functional Interface(函数式接口)
- 理解函数式接口是学习Java8,Lambda表达式的关键
- 函数式接口的定义:
- 任何接口如果只包含唯一一个抽象方法,那么它就是一个函数式接口
- 对于函数式接口,我们可以通过Lambda表达式来创建该接口的对象。
package com.thread.lambdaTest;
/*
* 推导Lambda表达式
* 注释的序号,是一个逐步简化的过程
* 1.定义一个函数式接口
* 2.用实现类实现接口
* 3.用静态内部类实现接口
* 4.用局部内部类实现接口
* 5.用匿名内部类实现接口
* 6.用lambda表达式简化实现接口
* */
//1.定义一个函数式接口
interface Love{
void showLove();
}
//2.实现类
class Person implements Love{
@Override
public void showLove() {
System.out.println("I love you");
}
}
public class LambdaTest {
//3.静态内部类
static class Person2 implements Love{
@Override
public void showLove() {
System.out.println("I love you 2");
}
}
public static void main(String[] args) {
//4.局部内部类
class Person3 implements Love{
@Override
public void showLove() {
System.out.println("I love you 3");
}
}
//5.匿名内部类,没有类的名称,必须借助接口或者父类
Love love4 = new Love() {
@Override
public void showLove() {
System.out.println("I love you 4");
}
};
//6.用lambda表达式简化
Love love5 = ()->{
System.out.println("I love you 5");
};
//测试实现类输出
Love love = new Person();
love.showLove();
//测试静态内部类输出
Love love2 = new Person2();
love2.showLove();
//测试局部内部类输出
Love love3 = new Person3();
love3.showLove();
//测试匿名内部类输出
love4.showLove();
//测试lambda表达式简化后输出
love5.showLove();
}
}
对于lambda表达式的简化
package com.thread.lambdaTest;
/*
* 继续简化lambda表达式
* */
//还是先定义接口
interface Kiss{
void kissYou(String name);
}
public class LambdaTestTwo {
public static void main(String[] args) {
//初始态
Kiss kiss = (String name)->{
System.out.println("I want to kiss you "+name);
};
kiss.kissYou("漂亮");
//简化方式一:参数类型 (两种写法:一种写在参数括号内部(只限单参数);另一种在参数括号外部)
Kiss kiss2 = (name -> {
//括号内
System.out.println("I want to kiss you "+name);
});
kiss2.kissYou("漂亮X2");
kiss2 = (name)->{
//括号外
System.out.println("I want to kiss you "+name);
};
kiss2.kissYou("漂亮X2.1");
//简化方式二:简化参数括号
Kiss kiss3 = name -> {
System.out.println("I want to kiss you "+name);
};
kiss3.kissYou("漂亮X3");
//简化方式三:简化代码外层花括号
Kiss kiss4 = name -> System.out.println("I want to kiss you "+name);
kiss4.kissYou("漂亮X4");
}
}
总结注意:
-
如若使用lambda表达式,必须保证接口是函数式接口(即该接口只有一个方法)
-
在对lambda表达式的简化方式一中,在参数括号内的写法,如果有大于一个以上的参数时,会报错。请使用括号外写法
-
简化方式三中,在只有一行代码的情况下可以缩减为一行。如果有多行代码,请使用简化方式一或二,用代码块包裹。
-
在有多个参数的情况下,也可以去掉参数类型。如果去掉,就都去掉。前提是所有参数必须写在括号内
5、多线程的五个状态
-
创建状态
线程对象一经创建就进入创建状态,此时它已经获取了相应的资源,但还没有处于可运行的状态。
-
就绪状态
当调用start()方法启动线程,线程就进入就绪状态,等待CPU调度执行
-
阻塞状态
调用sleep、wait或同步锁定时,线程就进入阻塞状态,该线程的代码将不会执行。当解除锁定后,该线程代码重新进入就绪状态等待CPU调度执行
-
运行状态
当就绪状态的线程获得CPU资源时,即可进入运行状态,线程才真正开始执行线程体的代码块
-
死亡状态
线程中断或者执行完毕,线程则进入死亡状态。处于死亡状态的线程不具有继续运行的能力
6、对于线程的操作
6.1停止线程的建议方法
package com.thread.stop;
/*
* 测试停止线程
* 1.建议线程正常停止--->利用次数,不建议死循环
* 2.建议使用标志位
* 3.不要使用stop(停止)或者destroy(破坏)等过时或者JDK不建议使用的方法
* */
public class StopThreadTest implements Runnable{
//1.设置一个标志位
private boolean flag = true;
@Override
public void run() {
int i = 1;
while (flag){
System.out.println("thread---run--->"+i++);
}
}
//2。设置一个公开的方法停止线程
public void stop() {
this.flag = false;
}
public static void main(String[] args) {
StopThreadTest stopThreadTest = new StopThreadTest();
new Thread(stopThreadTest).start();
for (int i = 1; i <= 1000; i++) {
System.out.println("main--run-->"+i);
if (i==500){
//调用stop方法切换标志位,让线程停止
stopThreadTest.stop();
System.out.println("Thread stop!!!=============================");
}
}
}
}
6.2、线程休眠sleep的使用
- sleep(时间) 指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException,如果任何线程中断了当前线程,当这个异常被抛出是,当前线程的中断状态被清除。
- sleep时间结束后,线程进入就绪状态
- sleep可以模拟网络延时,倒计时
- 每个对象都有一个锁,sleep不会释放锁
用线程休眠模拟倒计时
/*模拟倒计时*/
public static void main(String[] args) {
int num = 10;
while (true){
System.out.println(num);
if (num==0){
break;
}
num--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
用线程休眠模拟系统时间
/*模拟打印系统时间*/
public static void main(String[] args) {
//获取当前系统时间
Date date = new Date(System.currentTimeMillis());
int i = 1;
while (true){
i++;
if (i>10){
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
//更新
date = new Date(System.currentTimeMillis());
}
}
7、线程礼让yield
- 礼让线程,暂停当前线程执行,允许其他具有相同优先级的线程获得运行机会
- 将当前线程从运行状态转换为就绪状态
- 线程的礼让只是提供一种可能,不保证一定礼让成功。看CPU的心情进行重新调度
package com.thread.yield;
/**
* 测试线程礼让
* 礼让不一定成功,看CPU心情
* */
public class TestYield {
public static void main(String[] args) {
new Thread(new ThreadYield(),"A").start();
new Thread(new ThreadYield(),"B").start();
}
}
class ThreadYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->start");
//线程礼让
Thread.yield();
System.out.println(Thread.currentThread().getName()+"--->end");
}
}
/**
* 礼让成功会连续输出两个start 失败会输出开始和结束
* A--->start B--->start
* B--->start B--->end
* A--->end A--->start
* B--->end A--->end
* */
8、线程强制执行join
- join强制执行,待此线程执行完成后,在执行其他线程,其他线程阻塞。
package com.thread.join;
/*线程强制执行*/
public class JoinTest implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("VIP线程执行"+i+"次");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new JoinTest());
thread.start();
for (int i = 0; i < 500; i++) {
if (i==200){
System.out.println("===============================================");
thread.join();
}
System.out.println("普通线程执行"+i+"次");
}
}
}
9、线程状态state
-
NEW (新生)
- 尚未启动的线程处于此状态。
-
RUNNABLE
- 在Java虚拟机中执行的线程处于此状态。
-
BLOCKED
- 被阻塞等待监视器锁定的线程处于此状态。
-
WAITING
- 正在等待另一个线程执行特定动作的线程处于此状态。
-
TIMED_WAITING
- 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
-
TERMINATED
- 已退出的线程处于此状态。
package com.thread.state;
/*观察线程状态*/
public class StateTest {
public static void main(String[] args) {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("/");
});
//观察状态
Thread.State state = thread.getState();
System.out.println(state);
//启动后观察
thread.start();
state = thread.getState();
System.out.println(state);
//只要线程不终止就一直输出状态
while (state != Thread.State.TERMINATED){
try {
//每隔100毫秒输出一次状态
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新线程状态
state = thread.getState();
//输出状态
System.out.println(state);
}
}
}
10、线程的优先级Priority
- MAX_PRIORITY 最大优先级 = 10
- NORM_PRIORITY 默认优先级 = 5
- MIN_PRIORITY 最小优先级 = 1
线程的优先级高,只意味着获得调度的概率高,并不一定优先执行。小概率可能会出现性能倒置
package com.thread.priority;
/*测试线程优先级*/
public class PriorityTest {
public static void main(String[] args) {
Thread t1 = new Thread(new Priority(),"一");
Thread t2 = new Thread(new Priority(),"二");
Thread t3 = new Thread(new Priority(),"三");
Thread t4 = new Thread(new Priority(),"四");
Thread t5 = new Thread(new Priority(),"五");
System.out.println(
Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
/*先设置优先级,在启动*/
//设置优先级为10
t1.setPriority(Thread.MAX_PRIORITY);
t1.start();
//设置优先级为6
t2.setPriority(6);
t2.start();
//设置优先级为5
t3.setPriority(Thread.NORM_PRIORITY);
t3.start();
//设置优先级为4
t4.setPriority(4);
t4.start();
//设置优先级为3
t5.setPriority(3);
t5.start();
}
}
class Priority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
}
}
11、守护线程daemon
- 守护线程是为其他线程服务的线程;
- 所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出;
- 守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
package com.thread.daemon;
/*测试守护线程*/
//上帝守护人类
public class DaemonTest {
public static void main(String[] args) {
//把上帝放入线程中
Thread god = new Thread(new God());
//把上帝所在的线程设置为守护线程,默认为false
god.setDaemon(true);
//启动守护线程
god.start();
//启动用户线程
new Thread(new Person()).start();
}
}
//上帝,神
class God implements Runnable{
@Override
public void run() {
int i = 1;
while (true){
System.out.println("上帝保佑你"+(i++)+"年");
}
}
}
//人类
class Person implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println("我来到这个世界的第"+i+"年");
}
System.out.println("GoodBye,world!");
}
}
12、线程同步synchronized
- 多线程同时读写共享变量时,会造成逻辑错误,因此需要通过
synchronized
同步; - 同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 注意加锁对象必须是同一个实例;
- 对JVM定义的单个原子操作不需要同步。
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 安全和性能不可兼得
使用synchronized的两种方式
修饰方法
- 用
synchronized
修饰的方法就是同步方法,它表示整个方法都必须用this
实例加锁。 - 如果对一个静态方法添加
synchronized
修饰符,它锁住的是哪个对象?- 对于
static
方法,是没有this
实例的,因为static
方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class
实例,因此,对static
方法添加synchronized
,锁住的是该类的Class
实例。
- 对于
package com.thread.syn;
/*安全的买票*/
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"黄牛").start();
new Thread(buyTicket,"张三").start();
new Thread(buyTicket,"李四").start();
}
}
//买票
class BuyTicket implements Runnable{
//票数
private int ticketNums = 10;
//标识符
private boolean flag = true;
@Override
public void run() {
while (flag){
buy();
}
}
private synchronized void buy(){
//判断是否有票
if (ticketNums<=0){
flag = false;
return;
}
ticketNums--;
System.out.println(Thread.currentThread().getName()+"拿到第"+(10-ticketNums)+"张票,还有"+ticketNums+"张票");
}
}
修饰代码块
-
一个方法里的代码分为
只读代码
和进行数据操作的修改代码
,只需要给涉及到数据操作的代码上锁就可以 -
方法里面需要修改的内容才需要锁,锁太多影响性能,浪费资源
public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
13、拓展 CopyOnWriterArrayList
- 在同一时间多个线程无法对同一个List进行读取和增删,否则就会抛出并发异常
- CopyOnWriteArrayList完美解决的开发工作中的多线程的并发问题。
- 在读多写少的情况下使用
- 缺点:
- **内存占有问题:**两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大
- **数据一致性:**CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器
/*CopyOnWriteArrayList的应用*/
public class TestGUC {
public static void main(String[] args) {
//定义一个CopyOnWriteArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
//多线程循环遍历添加线程名
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
//让线程休眠一秒,保证数据的最终一致性
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出线程名的个数,看是否为10000个
System.out.println(list.size());
}
}
14、死锁
-
产生死锁的四个必要条件
- **互斥条件:**一个资源每次只能被一个线程使用
- **请求与保持条件:**一个进程引请求资源而阻塞时,对已获得的资源保持不放
- **不剥夺条件:**进程已获得的资源,在未使用完之前,不能强行剥夺
- **循环等待条件:**若干进程之间形成一种头尾相接的循环的等待资源关系。
-
只要破解以上四个条件中的任意一项或多项就可避免死锁。
案例:化妆
两个姑娘化妆时,在口红和镜子都只有一个的情况下。一个持有镜子,另一个持有口红。两人都需要化妆,然后都拿着自己所持有的资源,并且想要对方的资源,但对方都不愿意先交出自己的资源,就会形成如下死锁。
package com.thread.deadLock;
/*死锁:多个线程互相占用对方所需资源,形成僵持*/
public class DeadLock {
public static void main(String[] args) {
Makeup g1 = new Makeup(0,"白雪公主");
Makeup g2 = new Makeup(1,"灰姑娘");
g1.start();
g2.start();
}
}
//镜子
class Mirror{}
//口红
class Lipstick{}
//化妆
class Makeup extends Thread{
//用static来保证,所需的资源只有一份
static Mirror mirror = new Mirror();
static Lipstick lipstick = new Lipstick();
//选择
int select;
//女孩姓名
String girlName;
public Makeup(int select,String girlName){
this.select = select;
this.girlName = girlName;
}
@Override
public void run() {
if (select==0){
synchronized (mirror){
System.out.println(this.girlName+"获得镜子的使用权");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lipstick){
System.out.println(this.girlName+"获得口红的使用权");
}
}
}else {
synchronized (lipstick){
System.out.println(this.girlName+"获得口红的使用权");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mirror){
System.out.println(this.girlName+"获得镜子的使用权");
}
}
}
}
}
将run
方法内的代码修改为如下,即可解决问题
@Override
public void run() {
if (select==0){
synchronized (mirror){
System.out.println(this.girlName+"获得镜子的使用权");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (lipstick){
System.out.println(this.girlName+"获得口红的使用权");
}
}else {
synchronized (lipstick){
System.out.println(this.girlName+"获得口红的使用权");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (mirror){
System.out.println(this.girlName+"获得镜子的使用权");
}
}
}
15、终极lock锁
-
ReentrantLock
可重入锁 -
JDK5.0开始,java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现,同步锁使用
Lock
对象充当 -
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象 -
ReentrantLock
类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显方式加锁、释放锁
package com.thread.ultimate;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
BuyTickets buyTickets = new BuyTickets();
new Thread(buyTickets,"黄牛").start();
new Thread(buyTickets,"张三").start();
new Thread(buyTickets,"李四").start();
}
}
//买票
class BuyTickets implements Runnable{
//票数
int tickets = 10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
while (true){
//如果同步代码有异常,需要用finally块包裹lock.unlock();
try {
if (tickets>0){
Thread.sleep(500);
tickets--;
System.out.println
(Thread.currentThread().getName()+"买到第"+(10-tickets)+"张票,还剩"+tickets+"张票");
}else {
break;
}
} catch (InterruptedException e) {
//此处异常为Thread.sleep产生
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
synchronized与lock的对比
- Lock是显式锁(需要手动开启和关闭锁,切记关闭锁)synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程性能更好。并且具有更好的拓展性(提供更多的子类)
- 建议优先使用顺序:
- Lock>同步代码块(以及进入方法体,分配了相应资源)>同步方法(在方法体之外)
代码块锁 | 方法锁 | 更好的拓展性 | 性能更好 | |
---|---|---|---|---|
Lock(显式锁) | √ | √ | √ | |
synchronized(隐式锁) | √ | √ |
16、生产者和消费者问题
应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品,生产者将产品生产出来放入仓库中,消费者将从仓库中消费取走商品
- 如果仓库中没有产品,生产者需将产品放入仓库。否则停止生产并等待,等待消费者取走产品
- 如果仓库中有产品,消费者可以将产品取走消费。否则停止消费并等待,等待仓库中放入产品
这是一个线程同步,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
在生产者消费者问题中,仅有synchronized是不够的的
- synchronized可阻止并发更新同一个共享资源,实现了同步
- synchronized不能用来实现不同线程之间的消息传递(通信)
java提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知。与sleep不同,wait会释放锁 |
wait(long timeout) | 制定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait等待的线程,优先级高的线程,优先调度 |
注意:以上方法都是Object类的方法,都只能在同步方法或同步代码块中使用,否则会抛出异常IllegalMonitorStateException
解决方式一、管程法
- 生产者:负责生产数据的模块(可能是方法、对象、线程、进程)
- 消费者:负责处理数据的模块(可能是方法、对象、线程、进程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
package com.thread.producerConsumerProblem;
/*测试生产者与消费者模型 缓冲区解决,管程法*/
public class PCTest {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Producer(synContainer).start();
new Consumer(synContainer).start();
}
}
//生产者
class Producer extends Thread{
SynContainer synContainer;
public Producer(SynContainer synContainer){
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
synContainer.setProduct(new Phone(i));
System.out.println("生产了第"+i+"部手机");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer synContainer;
public Consumer(SynContainer synContainer){
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println("消费者购买了第"+synContainer.getProduct().id+"部手机");
}
}
}
//产品
class Phone{
//产品编号
int id;
public Phone(int id){
this.id = id;
}
}
//缓冲区
class SynContainer{
//容器大小
Phone [] phones = new Phone[5];
//容器计数器
int count = 0;
//生产者放入产品
public synchronized void setProduct(Phone phone){
//如果产品满了就需要等待消费者消费
if (count==phones.length){
//通知消费者消费,生产者等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有满就放入产品
phones[count] = phone;
count++;
//通知消费者消费
}
//消费者消费产品
public synchronized Phone getProduct(){
//判断能否消费
if(count==0){
//等待生产者生产,消费者等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消费
count--;
//通知生产者生产
return phones[count];
}
}
解决方式二、信号灯法
package com.thread.producerConsumerProblem;
public class PCTest2 {
public static void main(String[] args) {
Tv tv = new Tv();
new Actor(tv).start();
new Watcher(tv).start();
}
}
//演员
class Actor extends Thread{
Tv tv;
public Actor(Tv tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i%2==0){
this.tv.performance("快乐大本营");
}else {
this.tv.performance("抖音");
}
}
}
}
//观众watcher
class Watcher extends Thread{
Tv tv;
public Watcher(Tv tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.tv.watch();
}
}
}
//电视台
class Tv{
//节目
String program;
//标志位,信号灯
boolean flag = true;
//表演
public synchronized void performance(String program){
if (!flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员在表演:"+program);
//通知观众观看,唤醒
this.notifyAll();
this.program = program;
flag = !flag;
}
//观看
public synchronized void watch(){
if (flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众在看:"+program);
//提示演员表演
this.notifyAll();
flag = !flag;
}
}
17、线程池
线程池背景:经常创建和销毁使用量特别大的资源比如 并发情况下的线程,对性能影响很大。
使用线程池的优势:
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的真正实现类是ThreadPoolExecutor,其构造方法有如下4种:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
可以看到,其需要如下几个参数:
- corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
- maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
- keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
- unit(必需):指定keepAliveTime参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- workQueue(必需):任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该参数中。其采用阻塞队列实现。
- threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
- handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
线程池的使用流程如下:
// 创建线程池
Executor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
@Override
public void run() {
... // 线程执行的任务
}
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
上述线程池设置太麻烦?Executors
已经为我们封装好了4种常见的功能线程池
定长线程池(FixedThreadPool)
创建方法的源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
使用示例:
/*测试线程池*/
//创建线程池 参数为线程池大小
ExecutorService service = Executors.newFixedThreadPool(5);
//执行
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
//关闭链接
service.shutdown();
- 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
- 应用场景:控制线程最大并发数。
定时线程池(ScheduledThreadPool )
创建方法的源码:
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
使用示例:
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
- 特点:核心线程数量固定,非核心线程数量无限,执行完闲置10ms后回收,任务队列为延时阻塞队列。
- 应用场景:执行定时或周期性的任务。
可缓存线程池(CachedThreadPool)
创建方法的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
使用示例:
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
- 特点:无核心线程,非核心线程数量无限,执行完闲置60s后回收,任务队列为不存储元素的阻塞队列。
- 应用场景:执行大量、耗时少的任务。
单线程化线程池(SingleThreadExecutor)
创建方法的源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
使用示例:
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
- 特点:只有1个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
- 应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作、文件操作等。
参考
线程池部分