Java中的多线程
多线程
线程,进程,多线程
Process(进程)与 Thread(线程)
普通方法调用,只有主线程一条执行路径
多线程则是多条执行路径,主线程和子线程并行交替执行
一个进程可以有多个线程。
- 程序:程序是指令和数据的集合,其本身没有任何运行的含义,是一个静态的概念
- 进程:是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位
- 线程:线程是CPU调度和执行的单位,一个进程中至少有一个线程,可以有若干个线程
多线程:***真正的多线程是指有多个CPU,即多核,如服务器。***如果是模拟出来的多线程,即在一个CPU的情况下,在同一时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉
注意点:
- 线程就是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如main线程,gc线程
- main() 称之为主线程,是系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外开销,如CPU调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程创建
三种创建方式:
- Thread class:继承 Thread 类(重点)
- Runnable接口:实现 Runnable 接口(重点)
- Callable接口:实现 Callable 接口(仅作了解)
Thread
继承Thread类创建线程
使用 Thread 类创建线程的方法:
- 自定义线程类继承Thread类
- 重写 run() 方法,编写线程执行体
- 创建线程对象,调用 start() 方法启动线程
调用了start()方法不一定立即执行,因为线程需要CPU安排调度
public static void main(String[] args){
Demo thread = new Demo();
// 启动线程
thread.start();
for (int i = 0; i < 200; i++) {
System.out.println("这是main线程"+i);
}
}
@Override
public void run() {
// run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("这是run线程"+i);
}
}
// run方法中的输出语句,可能在main方法中的输出语句之后,也可能夹杂在其中,是因为CPU需要安排调度
main方法与run方法是交替执行的
例题:多线程下载网络图片
package test;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class TestThread extends Thread {
private String url;
private String name;
public TestThread(String url, String name) {
this.url = url;
this.name = name;
}
public void run() {
WebDownloadr webDownloadr = new WebDownloadr();
webDownloadr.downloader(url, name);
System.out.println("下载了文件,名为" + name);
}
public static void main(String[] args) {
TestThread t1 = new TestThread("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "1.jpg");
TestThread t2 = new TestThread("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "2.jpg");
TestThread t3 = new TestThread("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "3.jpg");
t1.start();
t2.start();
t3.start();
// 输出不一定为 1,2,3 。先下载完的会先显示
}
}
class WebDownloadr {
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("下载异常");
}
}
}
在 WebDownload 方法中使用的 FileUtils.copyURLToFile 方法,为封装好的工具包,
实现Runnable接口创建线程
- 定义 MyRunnable 类实现 Runnable 接口
- 实现 run() 方法,编写线程执行体
- 创建线程对线,调用 start() 方法启动线程
推荐使用 Runnable 接口来创建线程,这样可以避免程序被单继承限制
public class TestThread implements Runnable {
public static void main(String[] args){
// 创建实现了Runnable接口的对象
TestThread thread = new TestThread();
// 创建线程对象,通过线程对象来启动线程
new Thread(thread).start();
for (int i = 0; i < 200; i++) {
System.out.println("这是main线程"+i);
}
}
@Override
public void run() {
// run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("这是run线程"+i);
}
}
}
多个线程操作同一个对象
获得当前线程名字的方法
- Thread.currentThread().getName()
多个线程操作同一个资源数据时,数据可能出错,
// 代码中存在数据错误问题,在并发执行笔记中会进行解决
public class TestThread implements Runnable {
private int ticketNums = 10;
public static void main(String[] args) {
TestThread ticket = new TestThread();
new Thread(ticket, "小王").start();
new Thread(ticket, "小李").start();
new Thread(ticket, "小张").start();
}
@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}
try {
// 设置延时
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买到了" + ticketNums-- + "号票");
}
}
}
实现Callable接口创建线程
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool( 1 );
- 提交执行:Future< Boolean > result1 = ser.submit( t1 );
- 获取结果:boolean r1 = result1.get()
- 关闭服务:ser.shutdownNow();
Callable的好处:
- 可以设置返回值
- 可以抛出异常
package test;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
public class TestCallable implements Callable<Boolean> {
private String url;
private String name;
public TestCallable(String url, String name) {
this.url = url;
this.name = name;
}
public Boolean call() {
WebDownloadr webDownloadr = new WebDownloadr();
webDownloadr.downloader(url, name);
System.out.println("下载了文件,名为" + name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "1.jpg");
TestCallable t2 = new TestCallable("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "2.jpg");
TestCallable t3 = new TestCallable("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "3.jpg");
//创建执行服务,池子中有三个线程:
ExecutorService ser = Executors.newFixedThreadPool( 3 );
//提交执行:
Future< Boolean > r1 = ser.submit( t1 );
Future< Boolean > r2 = ser.submit( t2 );
Future< Boolean > r3 = ser.submit( t3 );
//获取call()方法返回的结果:
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();
//关闭服务:
ser.shutdownNow();
}
}
class WebDownloadr {
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("下载异常");
}
}
}
静态代理
静态代理模式的注意事项
- 真实对象与代理对象都要实现同一个接口
- 代理对象要代理真实角色
静态代理模式的好处
- 代理对象可以做许多真实对象所不能做的事
- 真实对象只需要做自己的事即可
package test;
public class StaticProxy {
public static void main(String[] args) {
// 代理角色接收真实角色
Chauffeur chauffeur = new Chauffeur(new Boss());
//new Thread(()-> System.out.println("A")).start();
//new Chauffeur(new Boss()).drive(); 实际上,Thread也是代理
chauffeur.drive();
}
}
// 定义接口
interface Drive{
void drive();
}
// 真实角色:Boss,只负责坐车,自己不开车
class Boss implements Drive{
@Override
public void drive() {
System.out.println("我是老板,我不开车");
}
}
// 代理角色:司机,帮你开车
class Chauffeur implements Drive{
private Drive target;
// 定义有参构造,接收 真实对象
public Chauffeur(Drive target) {
this.target = target;
}
@Override
public void drive() {
System.out.println("我是司机,我来开车");
talk();
// 真实对象实现的接口方法
this.target.drive();
arrive();
}
private void arrive() {
System.out.println("老板,已经到了目的地");
}
private void talk() {
System.out.println("老板,您想去哪里");
}
}
线程的状态与方法
线程中的方法
- setPriority( int newPriority ):更改线程的优先级
- static void sleep( long millis ) :在指定的毫秒数内让线程休眠
- void join():等待线程终止
- static void yield():暂停当前正在执行的线程,并执行其他线程
- void interrupt():中断线程,一般不会使用这个方法
- boolean isAlive():判断线程是否处于活动状态
线程停止
终止线程,建议使用标志位方式。stop()和destory()方法已经是过时方法
package test;
public class TestThread implements Runnable {
// 设置标志位
private boolean flag = true;
// 自行设置停止方法
public void stop() {
this.flag = false;
}
@Override
public void run() {
int i = 0;
while (flag) {
System.out.println("线程运行" + i++);
}
}
public static void main(String[] args) {
TestThread thread = new TestThread();
new Thread(thread).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main运行" + i);
if (i == 800) {
// 调用stop方法切换标志位,让线程停止
thread.stop();
System.out.println("线程停止了");
}
}
}
}
线程休眠
- sleep方法中的参数表示指定当前线程阻塞的毫秒数
- sleep存在 InterruptedException 异常
- sleep的休眠时间达到后,线程会进入就绪状态
- sleep可以用来模拟网络延时,倒计时等操作
- 每一个对象都有一个锁,sleep并不会释放锁
模拟网络延时的作用:放大问题的发生性
public class TestSleep {
public static void main(String[] args) throws InterruptedException {
// 获取当前时间
Date date = new Date(System.currentTimeMillis());
// 每隔一秒输出当前时间方法
while (true) {
// 休眠一秒
Thread.sleep(1000);
// 设置时间格式,并且输出时间
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
// 更新时间
date = new Date(System.currentTimeMillis());
}
// countTime();
}
// 倒计时方法
public static void countTime() throws InterruptedException {
int num = 10;
while (true) {
Thread.sleep(1000);
System.out.println(num);
num--;
if (num <= 0) {
break;
}
}
}
}
线程礼让
- 礼让线程:让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转换为就绪状态
- 让CPU重新调度,礼让不一定成功,这取决于CPU的调度顺序
public class TestYield {
public static void main(String[] args){
MyYield myYield = new MyYield();
new Thread(myYield,"ThreadOne").start();
new Thread(myYield,"ThreadTwo").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
// 进行礼让
// 正常情况下为 ThreadOne开始执行,ThreadOne停止执行,然后是Two
// 可能礼让成功,也可能礼让不成功
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程停止执行");
}
}
join
- join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
- 相当于插队,尽量少使用,避免造成线程阻塞
public class TestJoin implements Runnable {
public static void main(String[] args) throws InterruptedException {
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
for (int i = 0; i < 100; i++) {
if (i == 70) {
thread.join();
}
System.out.println("main" + i);
}
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("VIP" + i);
}
}
}
观测线程状态
Thread.getState():获取指定线程当前状态
Thread.State state = thread.getState();
- NEW:尚未启动状态
- RUNNABLE:执行状态
- BLOCKED:阻塞状态
- WAITING:等待状态,正在等待另一个线程执行特定动作
- TIMED_WAITING:等待状态,正在等待另一个线程执行动作达到指定等待时间
- TERMINATED:已经退出
一个线程可以在给定时间点处于一个状态,这些状态是不反映任何操作系统线程状态的虚拟机状态
public static void main(String[] args) throws InterruptedException {
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
state = thread.getState();
System.out.println(state);
// 如果线程不是终止状态
while (state != Thread.State.TERMINATED) {
// 每200毫秒输出一次线程状态
Thread.sleep(200);
// 更新状态,先更新,再输出
state = thread.getState();
System.out.println(state);
}
}
线程优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
线程优先级必须在线程启动前设置
线程的优先级用数字表示,范围为1-10
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
改变线程优先级的方法
- getPriority()
- setPriority( int xxx )
优先级越高,也不一定先执行,主要看CPU的调度,只不过优先级高了,先执行的几率更大
public class TestPriority {
public static void main(String[] args){
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority, "t1");
Thread t2 = new Thread(myPriority, "t2");
Thread t3 = new Thread(myPriority, "t3");
Thread t4 = new Thread(myPriority, "t4");
Thread t5 = new Thread(myPriority, "t5");
// 输出main方法的优先级,他只有默认优先级,默认为5
System.out.println("我是:" + Thread.currentThread().getName() + "\t我的优先级为:" + Thread.currentThread().getPriority());
// 设置优先级
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(7);
t3.setPriority(2);
t4.setPriority(Thread.MAX_PRIORITY);
// 测试错误的优先级,小于0或者大于10,都是错误的
// t5.setPriority(-2);
// 启动线程
t1.start();
t2.start();
t3.start();
t4.start();
// t5.start();
/*我是:main 我的优先级为:5
我是:t4 我的优先级为:10
我是:t2 我的优先级为:7
我是:t3 我的优先级为:2
我是:t1 我的优先级为:1*/
}
}
class MyPriority implements Runnable {
@Override
public void run() {
System.out.println("我是:" + Thread.currentThread().getName() + "\t我的优先级为:" + Thread.currentThread().getPriority());
}
}
守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 例如监控内存,垃圾回收等,就是守护线程
设置守护线程的方法
- Thread.setDaemon( boolean on ):设置为true则为守护线程,默认为false
public class TestDaemon {
public static void main(String[] args){
Thread thread = new Thread(new God());
// 设置为God为守护线程
thread.setDaemon(true);
thread.start();
// 启动Person线程
new Thread(new Person()).start();
}
}
class God implements Runnable {
@Override
public void run() {
// 虽然条件一直为真,但是守护线程并不会一直执行,用户线程结束之后,守护线程也会随之结束
while(true){
System.out.println("God always be with you");
}
}
}
class Person implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("I am " + i + "years old");
}
System.out.println("Now i'm dead");
}
}
Lamda表达式
lamda 表达式是JDK8的新特性。为什么要使用 lamda 表达式
- 避免匿名内部类定义过多
- 让代码看起来更简洁
- 去除一部分无意义代码
Functional Interface ( 函数式接口 )是学习 lamda 表达式的关键所在
函数式接口的定义:
任何***只包含唯一 一个抽象方法***的接口,如下
public interface Runnable{
public abstract void run();
}
就是函数式接口对于函数式接口,我们可以通过 lamda 表达式来创建该接口的对象
package test;
// Lamda表达式的推导
public class TestLamda {
// 3.定义静态内部类
static class Study2 implements Lamda {
@Override
public void lamda() {
System.out.println("Lamda lesson 2");
}
}
public static void main(String[] args) {
new Study().lamda();
new Study2().lamda();
// 4.定义局部内部类
class Study3 implements Lamda {
@Override
public void lamda() {
System.out.println("Lamda lesson 3");
}
}
new Study3().lamda();
// 5.定义匿名内部类
Lamda study = new Lamda() {
@Override
public void lamda() {
System.out.println("Lamda lesson 4");
}
};
study.lamda();
// 6.用Lamda简化
study = () -> {
System.out.println("Lamda lesson 5");
};
study.lamda();
}
}
// 1.定义 函数式接口
interface Lamda {
void lamda();
}
// 2.定义外部类
class Study implements Lamda {
@Override
public void lamda() {
System.out.println("Lamda lesson 1");
}
}
Lamda表达式的直观使用
package test;
public class TestLamda {
public static void main(String[] args) {
Lamda person = (name) -> {
System.out.println(name + "喜欢lamda表达式");
};
person.lamda("张三");
// 张三喜欢lamda表达式
}
}
interface Lamda {
void lamda(String name);
}
lamda表达式的简化
-
lamda表达式可以将参数类型去除
-
当函数式接口中的方法只有一个参数时,lamda表达式可以将小括号去除,当方法中只有一句代码时,可以将花括号去除,如下:
public class TestLamda { public static void main(String[] args) { Lamda person = name -> System.out.println(name + "喜欢lamda表达式"); person.lamda("张三"); } } interface Lamda { void lamda(String name); }
-
当方法中有两个参数时,可以去除参数类型,但是不能去除小括号,如果方法中的代码语句不止一条,那么需要加上花括号,如下:
public class TestLamda { public static void main(String[] args) { Lamda person = (name, age) -> { System.out.println("我今年" + age + "岁"); System.out.println(name + "喜欢lamda表达式"); }; person.lamda("张三", 20); } } interface Lamda { void lamda(String name, int age); }
因为Runnaable是一个函数式接口,只有一个run方法,所以可以用lamda表达式来简化他的实现类。
线程同步
并发:同一个对象被多个线程同时操作,例如抢票
处理多线程问题时,如果有多个进程访问同一个对象,并且还存在修改对象的行为,那么就需要线程同步。
线程同步实际上是一种排队机制,让需要访问同一个对象的线程进入对象的等待池,形成队列,排队访问资源
队列和锁
线程同步需要队列加锁来实现
为了保证数据的安全性,多线程访问数据需要加入锁机制,synchronized,当一个线程获得对象的排他锁时,它可以独享对象,其他线程必须等待,在使用完资源之后 ,需要释放锁。
锁机制带来的问题:
- 一个线程持有锁会导致其他所有访问该资源的线程挂起
- 较多线程访问统一资源时,频繁的加锁,释放锁会导致较多的上下文切换,调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
例题:银行中不安全的取钱问题
package test;
public class TestSyn {
public static void main(String[] args) {
Account account = new Account(100, "存款");
DrawMoney boy = new DrawMoney(account, 50, 0, "男孩");
DrawMoney girl = new DrawMoney(account, 100, 0, "女孩");
new Thread(boy).start();
new Thread(girl).start();
}
}
class Account {
// 卡里的钱
int money;
// 卡号
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class DrawMoney implements Runnable {
// 实例化一个账号对象
Account account;
// 取走的钱
private int drawMoney;
// 手里的钱
private int nowMoney;
String name;
public DrawMoney(Account account, int drawMoney, int nowMoney, String name) {
this.name = name;
this.account = account;
this.drawMoney = drawMoney;
this.nowMoney = nowMoney;
}
@Override
public void run() {
// 让线程休眠一秒,增大问题发生性
// 如果取的钱大于余额,则跳出方法
if ((account.money - drawMoney) < 0) {
System.out.println(this.name + "无法取钱,余额不足");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money - drawMoney;
nowMoney = nowMoney + drawMoney;
System.out.println("余额为" + account.money + this.name + "手上有" + nowMoney);
}
}
同步
synchronized关键字,它有两种用法,synchronized方法和synchronized块
同步方法
- 同步方法:public synchronized void method( int args ){ }
synchronized方法控制对对象的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
缺点:如果将一个大的方法申明为synchronized ,会影响他的效率
一般只在方法中需要修改的资源加锁,锁的内容太多,会浪费资源
package test;
public class TestThread implements Runnable {
private int ticketNums = 10;
boolean flag = true;
public static void main(String[] args) {
TestThread ticket = new TestThread();
new Thread(ticket, "小王").start();
new Thread(ticket, "小李").start();
new Thread(ticket, "小张").start();
}
@Override
public void run() {
while (flag){
buy();
}
}
// 增加 sychronized 修饰符,设置同步方法
public synchronized void buy(){
if (ticketNums <= 0) {
flag =false;
return;
}
try {
// 设置延时
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买到了" + ticketNums-- + "号票");
}
}
synchronized 锁的是this对象(this所在的当前类)
同步静态方法的锁对象是 类名.class
同步块
synchronized ( Obj ) { }
Obj称为同步监视器
- Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
一般挑选的加锁对象,为数据发生变化的对象(进行了增删改)
package test;
public class TestSyn {
public static void main(String[] args) {
Account account = new Account(100, "存款");
DrawMoney boy = new DrawMoney(account, 50, 0, "男孩");
DrawMoney girl = new DrawMoney(account, 100, 0, "女孩");
new Thread(boy).start();
new Thread(girl).start();
}
}
class Account {
// 卡里的钱
int money;
// 卡号
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class DrawMoney implements Runnable {
// 实例化一个账号对象
Account account;
// 取走的钱
private int drawMoney;
// 手里的钱
private int nowMoney;
String name;
public DrawMoney(Account account, int drawMoney, int nowMoney, String name) {
this.name = name;
this.account = account;
this.drawMoney = drawMoney;
this.nowMoney = nowMoney;
}
@Override
public void run() {
// 同步代码块,对象为 account
synchronized (account){
// 如果取的钱大于余额,则跳出方法
if ((account.money - drawMoney) < 0) {
System.out.println(this.name + "无法取钱,余额不足");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money - drawMoney;
nowMoney = nowMoney + drawMoney;
System.out.println("余额为" + account.money + this.name + "手上有" + nowMoney);
}
}
}
如果一段代码分成两段,两次锁的对象不同,则无法保证安全性。
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没上锁,锁定同步监视器并且访问
锁
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情况.某一个同步块同时拥有两个以上对象的锁时,就可能会发生死锁的问题
简单来说,就是 多个线程互相拥有者对方所需要的资源,形成僵持状态
package test;
public class TestLock {
public static void main(String[] args) {
Eat lbxx = new Eat(0, "蜡笔小新");
Eat fjc = new Eat(1, "风间彻");
new Thread(lbxx).start();
new Thread(fjc).start();
}
}
class Eat implements Runnable {
int choice;
String name;
static Dish dish = new Dish();
static Chopsticks chopsticks = new Chopsticks();
Eat(int choice, String name) {
this.choice = choice;
this.name = name;
}
@Override
public void run() {
try {
eat();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void eat() throws InterruptedException {
if (choice == 0) {
// 获得dish锁
synchronized (dish) {
System.out.println(this.name + "获得了盘子");
// 设置延时,放大问题发生性
Thread.sleep(1000);
// 想要获得chopsticks锁
synchronized (chopsticks) {
System.out.println(this.name + "想获得筷子");
}
}
} else {
synchronized (chopsticks) {
System.out.println(this.name + "获得了筷子");
Thread.sleep(2000);
synchronized (dish) {
System.out.println(this.name + "想获得盘子");
}
}
}
}
}
// 模拟两个人,一个人拿到了盘子,一个人拿到了筷子,都想要吃饭
// 盘子
class Dish {
}
// 筷子
class Chopsticks {
}
最终,会产生死锁,因为各自拥有对方想要的资源,互相僵持
产生死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
只要其中任意一个条件或多个条件能被打破,那么死锁就不会产生
Lock
Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前先获得Lock对象
ReentrantLock ( 可重入锁 ) 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
public class TestLock {
public static void main(String[] args) {
sellTickets sellTickets = new sellTickets();
new Thread(sellTickets).start();
new Thread(sellTickets).start();
new Thread(sellTickets).start();
}
}
class sellTickets implements Runnable {
int tickNums = 10;
// 定义 可重入 锁,使用 private final 修饰,保证安全性
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
// 加锁
lock.lock();
if (tickNums > 0) {
Thread.sleep(1000);
System.out.println(tickNums--);
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
}
}
一般解锁都写在finally里面
synchronized与Lock的对比
- Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域则自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且有更好的扩展性(子类更多)
锁的优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
线程协作
生产者消费者问题
生产者消费者问题场景:
假设一个仓库中只能存放一件商品,生产者将生产出的产品放入仓库,消费者从仓库中取走产品。如果仓库中没产品,则由生产者生产产品放入仓库,消费者等待;如果仓库中有产品,则消费者取走产品,生产者停止生产并且等待,直到产品被取走,才继续生产。消费者等到仓库中有产品,才能再次取走,如此循环。
实际上,这是一个线程同步的问题,生产者和消费者共享同一个资源,并且生产者和消费者之间互相依赖,互为条件
在生产者消费者问题中,仅有synchronized是不够的,
- synchronized 可阻止并发更新同一个共享资源,实现了同步
- synchronized 不能用来实现不同线程之间的消息传递(通信)
线程通信
线程通信之间的常用方法
- wait():表示线程一直等待,直到其他线程通知,与sleep不同的是,它会释放锁
- wait( long timeout ):指定等待的毫秒数
- notify():唤醒一个处于等待状态的线程
- notifyAll():唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度
注意:以上都是Object 类的方法,只能在同步方法或者同步代码块中使用,否则会抛出 IIIegalMonitorStateException 异常
解决生产者消费者问题
管程法
利用缓冲区解决问题
package test;
// 管程法解决生产者消费者问题
public class BoxTest {
public static void main(String[] args) {
Box box = new Box();
Producer producer = new Producer(box);
Customer customer = new Customer(box);
Customer customer1 = new Customer(box);
Customer customer2 = new Customer(box);
new Thread(producer).start();
new Thread(customer,"蜡笔小新").start();
new Thread(customer1,"樱桃小丸子").start();
new Thread(customer2,"犬夜叉").start();
}
}
// 牛奶,是共享资源
class Milk {
// 牛奶编号
int id;
Milk(int id) {
this.id = id;
}
}
// 奶箱,是缓冲区
class Box {
// 设置缓冲区大小为10,意味着同一时间内,缓冲区中最多有10个产品,
// 因为Milk[9]表示长度为9,但是在存入的时候,是从milk[0]开始存入,所以设置 Milk[9]来让缓冲区为10
Milk[] milks = new Milk[9];
// 容器计数器
int count = 0;
// 生产者生产产品
public synchronized void push(Milk milk,int i) {
// 如果容器满了,则需要等待消费者进行消费
while (count == milks.length) {
// 通知消费者消费,生产者等待
try {
// 让生产者进行等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有满,则生产产品,从0开始放入
milks[count] = milk;
// 生产一个产品,计数器增加一次
count++;
// 此时输出语句,则不会在控制台出现抢先消费的问题
System.out.println("生产了第" + (i) + "瓶牛奶");
// 已经生产了,有产品,就可以通知消费者进行消费
// 唤醒因为产品为空而进入等待的消费者
this.notifyAll();
}
// 消费者消费产品,返回一个产品对象
public synchronized Milk pop() {
// 判断是否有产品
while (count == 0) {
// 没有产品,则等待生产者进行生产,消费者等待
try {
// 让消费者进行等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果有数据,则进行消费
count--;
// 取出产品
Milk milk = milks[count];
// 消费完,通知生产者生产
// 唤醒因为产品满了而进入等待的生产者
this.notifyAll();
return milk;
}
}
// 生产者
class Producer implements Runnable {
// 获取奶箱对象
Box b;
Producer(Box b) {
this.b = b;
}
// 生产牛奶方法
@Override
public void run() {
// 生产100瓶牛奶,奶箱只有10的大小,所以到了10,应该通知消费者进行消费
for (int i = 0; i < 100; i++) {
// 把i作为牛奶的ID传入,生产牛奶
// 传入一个(i+1)作为输出参数,由于下标从0开始,但是输出不能从0开始,所以i需要加1
b.push(new Milk(i),(i+1));
}
}
}
// 消费者
class Customer implements Runnable {
// 获取奶箱对象
Box b;
Customer(Box b) {
this.b = b;
}
// 消费牛奶方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 获得消费方法中返回的产品对象的id
// 由于在生产者方法中,生产时存入的下标为从0开始,所以输出要加1
int id = b.pop().id;
System.out.println(Thread.currentThread().getName()+"消费了第" + (id + 1) + "瓶牛奶");
}
}
}
在最开始的情况下,push()方法中的 System.out.println(“生产了第” + (i) + “瓶牛奶”); 是写在生产者的run方法中的,原来的方法如下:
public void run() {
// 生产100瓶牛奶,奶箱只有10的大小,所以到了10,应该通知消费者进行消费
for (int i = 0; i < 100; i++) {
// 把i作为牛奶的ID传入,生产牛奶
// 传入一个(i+1)作为输出参数,由于下标从0开始,但是输出不能从0开始,所以i需要加1
b.push(new Milk(i));
System.out.println("生产了第" + (i+1) + "瓶牛奶");
}
}
在这种情况下,控制台的输出可能会出现超前消费的现象(消费未生产的产品)
原因如下:
由于线程通过start()启动,生产者先调用push()方法进行生产产品,然而同时,消费者也调用了pop()方法,但是他发现没有产品,只能进行等待,此时生产者在push()方法中,创建了一个新对象,由于push()方法和pop()方法都存在于共享缓冲区里面,而各自的输出语句都在各自的对象run()方法中,此时我们可以认为,线程都在同一个方法中,消费者在等待,此时由于先执行了count++,消费者立马认为缓冲区中是存在产品的,迅速进行了消费,先行输出了消费语句,生产语句才随之输出。
所以将输出语句放入到push方法中,即可避免出现超前消费的现象。
同理,消费者的run方法中也存在着输出语句,所以会出现超前生产的现象,原理同上,由于pop方法中不方便传入id参数,(会导致连续调用两次pop方法)所以此处并没有对这个问题进行解决
信号灯法
通过设置标志位来通知线程何时等待,何时唤醒
package test;
import java.nio.file.Path;
// 管程法解决生产者消费者问题
public class BoxTest {
public static void main(String[] args) {
DriveCar driveCar = new DriveCar();
Teacher teacher = new Teacher(driveCar);
Student student = new Student(driveCar);
new Thread(teacher).start();
new Thread(student).start();
}
}
// 驾校教练
class Teacher implements Runnable {
DriveCar driveCar;
Teacher(DriveCar driveCar) {
this.driveCar = driveCar;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
this.driveCar.TeacherDrive("AE86");
} else {
this.driveCar.TeacherDrive("五菱宏光");
}
}
}
}
// 学员
class Student implements Runnable {
DriveCar driveCar;
Student(DriveCar driveCar) {
this.driveCar = driveCar;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
this.driveCar.StudentDrive();
}
}
}
// 车
class DriveCar {
// 教练开车,学员等待;学员开车,教练等待
// 开的车
String car;
// 标志位
boolean flag = true;
// 教练开车
public synchronized void TeacherDrive(String car) {
// 如果标志位为假,说明学员在开车
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("学员没在开" + car);
System.out.println("教练开了" + car);
// 教练开完之后,让学员开
this.notifyAll();
this.car = car;
this.flag = !this.flag;
}
// 学员开车
public synchronized void StudentDrive() {
// 如果标志位为真,说明教练在开车
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("教练没在开" + car);
System.out.println("学员开了" + car);
// 让教练来开车
this.notifyAll();
this.flag = !this.flag;
}
}
线程池
由于多线程中经常创建和销毁、使用量特别大的资源,如并发情况下的线程,对性能的影响很大,所以需要使用线程池来提升性能。
线程池的思路
提前创建好多个线程,放入线程池中,需要使用时直接获取,使用完放回线程池,这样可以避免频繁的创建和销毁,实现反复利用
线程池的好处
- 提高相应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中的线程,不需要每次都重新创建)
- 便于线程管理
线程池的接口和工具类
ExecutorService:真正的线程池接口,常见子类 ThreadPoolExecutor
- void execute( Runnable command ):执行任务/命令,没有返回值 ,一般用来执行Runnable
- < T > Future < T > submit( Callable < T > task ):执行任务,有返回值,一般用来执行 Callable
- void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
package test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestPool {
public static void main(String[] args) {
// 1.创建服务,创建线程池,线程池大小为10
ExecutorService service = Executors.newFixedThreadPool(10);
// 执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
// 2.关闭连接
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "运行到" + i);
}
}
}