1. 线程简介
1.1 普通线程与多线程
1.2 程序、进程、线程
简单介绍
程序:程序是指令和数据的有序集合,其本身没有任何运行的意义,是一个静态的概念
进程(Process): 进程是执行程序的一次执行过程,他是一个动态的概念。是系统资源分配的单位
线程(Thread):一个进程中至少有一个线程,也可包含多个线程。线程是CPU调度和执行的单位
核心概念
(1)线程是独立的执行路径
(2)程序运行时,即使没有创建线程,后台也有,如主线程、GC线程
(3)mian()称为主线程,是系统的入口,用于执行整个程序
(4)进程中,若开辟多个线程,则线程的运行由调度器按排。调度器是与操作系统密切相关的,先后顺序可以认为无法人为干预
(5)对同一份资源操作时,会存在资源抢夺问题,需要加入并发控制
(6)线程会导致额外的开销,如cpu调度时间,并发控制开销
(7)每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
2. 线程实现(重点)
2.1 线程创建的三种方式
2.1.1 Thread class:继承Thread类(重点)
1. 自定义线程类,继承Thread类
2. 重写run方法,编写线程执行体
3. 创建线程对象,调用start()方法启动线程
// 创建线程方式1:继承Thread类,重写run()方法,调用start开启线程
public class ThreadDemo01 extends Thread{
// 重写run()方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("
Thread01执行第" + i + "次");
}
}
public static void main(String[] args) {
// main方法,主线程
for (int i = 0; i < 100; i++) {
System.out.println("main方法执行第" + i + "次");
}
// 创建新线程对象
ThreadDemo01 threadDemo01 = new ThreadDemo01();
// 调用start开启线程
threadDemo01.start();
}
}
2.1.2 Runable接口:实现Runable接口(重点)
2.1.3 Callable接口:实现Callable接口(了解)
https://blog.csdn.net/zsj777/article/details/85089993
2.2 代理模式
Proxy代理模式是一种结构型设计模式,主要解决的问题是:在直接访问对象时带来的问题。
代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。
更通俗的说,代理解决的问题当两个类需要通信时,引入第三方代理类,将两个类的关系解耦,让我们只了解代理类即可,而且代理的出现还可以让我们完成与另一个类之间的关系的统一管理,但是切记,代理类和委托类要实现相同的接口,因为代理真正调用的还是委托类的方法。
使用一个代理对象将对象包装起来,然后用该代理对象来取代该对象,任何对原始对象的调用都要通过代理,代理对象决定是否以及何时调用原始对象的方法
2.2.1 静态代理
静态代理类优缺点
优点:
代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合)
缺点:
1)代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
2)代理对象只服务于一种类型的对象,如果要服务多类型的对象,势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。
package thread.staticProxy;
/**
* 静态代理模式总结:
* 真实对象与代理对象都要实现同一个接口
* 代理对象要代理真实角色
*
* 好处:
* 代理对象可以做真实对象做不了的事情
* 真实对象只需要做自己的事情
*/
public class StaticProxyDemo01 {
public static void main(String[] args) {
AnimelProxy cat = new AnimelProxy(new Cat());
AnimelProxy dog = new AnimelProxy(new Dog());
cat.action();
System.out.println("===================");
cat.breath();
System.out.println("===================");
cat.food();
System.out.println("===================");
dog.action();
System.out.println("===================");
dog.breath();
System.out.println("===================");
dog.food();
}
}
// 接口
interface Animel{
void action();
void breath();
void food();
}
// 真实对象
class Cat implements Animel{
@Override
public void action() {
System.out.println("喵喵喵");
}
@Override
public void breath() {
System.out.println("呼噜呼噜");
}
@Override
public void food() {
System.out.println("爱吃鱼");
}
}
// 真实对象
class Dog implements Animel{
@Override
public void action() {
System.out.println("汪汪汪");
}
@Override
public void breath() {
System.out.println("呼呼呼");
}
@Override
public void food() {
System.out.println("爱啃骨头");
}
}
// 代理对象
class AnimelProxy implements Animel{
private Animel target;
public AnimelProxy(Animel target) {
this.target = target;
}
@Override
public void action() {
System.out.println("静态代理-->action方法开始执行");
target.action();
System.out.println("静态代理-->action方法执行结束");
}
@Override
public void breath() {
System.out.println("静态代理-->breath方法开始执行");
target.breath();
System.out.println("静态代理-->breath方法执行结束");
}
@Override
public void food() {
System.out.println("静态代理-->food方法开始执行");
target.food();
System.out.println("静态代理-->food方法执行结束");
}
}
静态代理-->action方法开始执行
喵喵喵
静态代理-->action方法执行结束
===================
静态代理-->breath方法开始执行
呼噜呼噜
静态代理-->breath方法执行结束
===================
静态代理-->food方法开始执行
爱吃鱼
静态代理-->food方法执行结束
===================
静态代理-->action方法开始执行
汪汪汪
静态代理-->action方法执行结束
===================
静态代理-->breath方法开始执行
呼呼呼
静态代理-->breath方法执行结束
===================
静态代理-->food方法开始执行
爱啃骨头
静态代理-->food方法执行结束
2.2.2 动态代理(提了一下)
引入动态代理:
根据如上的介绍,你会发现每个代理类只能为一个接口服务,这样程序开发中必然会产生许多的代理类。
所以我们就会想办法可以通过一个代理类完成全部的代理功能,那么我们就需要用动态代理。
在上面的示例中,一个代理只能代理一种类型,而且是在编译器就已经确定被代理的对象。而动态代理是在运行时,通过反射机制实现动态代理,并且能够代理各种类型的对象。
在Java中要想实现动态代理机制,需要java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy 类的支持。
2.3 Lamda表达式
2.3.1 作用
作用:避免匿名内部类定义过多。其实质属于函数式编程思想
(params)->expression[表达式]
(params)->ststement[语句]
(params)->{statements}
new Thread( ()->System.out.println("开启多线程") ).start();
为什么要用Lambda表达式
避免匿名内部类看起来过多
可以让代码更简洁
去掉了没有意义的代码,只留下核心逻辑
2.3.2 函数式接口
函数式接口(Functional Interface):Java 8出现的
定义:任何接口,如果只包含唯一一个抽象方法,那么他就是一个函数式接口
对于函数式接口,我们可以通过lambda表达式来创建该接口的对象
2.3.3 不带参数的例子
package thread.lambda;
/**
* 不带参数的lambda表达式
*/
public class LambdaDemo01 {
// 3. 使用静态内部类
static class Like2 implements ILike{
@Override
public void lambda() {
System.out.println("I Like Lambda 2");
}
}
public static void main(String[] args) {
// 2. 实现类
Like like1 = new Like();
like1.lambda(); // I Like Lambda 1
// 3. 使用静态内部类
Like2 like2 = new Like2();
like2.lambda(); // I Like Lambda 2
// 4. 使用局部内部类
class Like3 implements ILike{
@Override
public void lambda() {
System.out.println("I Like Lambda 3");
}
}
Like3 like3 = new Like3();
like3.lambda(); // I Like Lambda 3
// 5. 匿名内部类
ILike like4 = new ILike(){
@Override
public void lambda() {
System.out.println("I Like Lambda 4");
}
};
like4.lambda(); // I Like Lambda 4
// 6. lambda表达式
ILike like5 = () -> {
System.out.println("I Like Lambda 5");
};
like5.lambda(); // I Like Lambda 5
}
}
// 1. 创建接口
interface ILike{
void lambda();
}
// 2. 实现类
class Like implements ILike{
@Override
public void lambda() {
System.out.println("I Like Lambda 1");
}
}
2.3.4 带参数的例子
package thread.lambda;
/**
* 带参数的lambda表达式
*/
public class LambdaDemo02 {
// 3. 使用静态内部类
static class LikeLambda2 implements ILikeLambda{
@Override
public void lambda(String str) {
System.out.println("I Like Lambda 2 --> " + str);
}
}
public static void main(String[] args) {
// 2. 实现类
LikeLambda like1 = new LikeLambda();
like1.lambda("2. 实现类"); // I Like Lambda 1 --> 2. 实现类
// 3. 使用静态内部类
LikeLambda2 like2 = new LikeLambda2();
like2.lambda("3. 使用静态内部类"); // I Like Lambda 2 --> 3. 使用静态内部类
// 4. 使用局部内部类
class LikeLambda3 implements ILikeLambda{
@Override
public void lambda(String str) {
System.out.println("I Like Lambda 3 --> " + str);
}
}
LikeLambda3 like3 = new LikeLambda3();
like3.lambda("4. 使用局部内部类"); // I Like Lambda 3 --> 4. 使用局部内部类
// 5. 匿名内部类
ILikeLambda like4 = new ILikeLambda(){
@Override
public void lambda(String str) {
System.out.println("I Like Lambda 4 --> " + str);
}
};
like4.lambda("5. 匿名内部类"); // I Like Lambda 4 --> 5. 匿名内部类
// 6. lambda表达式,未简化形式
ILikeLambda like5 = (str) -> {
System.out.println("I Like Lambda 5 --> " + str);
};
like5.lambda("6. lambda表达式,未简化形式"); // I Like Lambda 5 --> 6. lambda表达式,未简化形式
// 7. lambda表达式,去掉小括号
// 注:只有一个参数的时候才能简化去掉小括号
ILikeLambda like6 = str -> {
System.out.println("I Like Lambda 5 --> " + str);
};
like6.lambda("7. lambda表达式,去掉小括号"); // I Like Lambda 5 --> 7. lambda表达式,去掉小括号
// 8. lambda表达式,去掉大括号
// 注:只有一行的时候才能简化去掉大括号
ILikeLambda like7 = str -> System.out.println("I Like Lambda 5 --> " + str);
like7.lambda("8. lambda表达式,去掉大括号"); // I Like Lambda 5 --> 8. lambda表达式,去掉大括号
}
}
// 1. 创建接口
interface ILikeLambda{
void lambda(String str);
}
// 2. 实现类
class LikeLambda implements ILikeLambda{
@Override
public void lambda(String str) {
System.out.println("I Like Lambda 1 --> " + str);
}
}
3. 线程状态
五大状态:
方法 | 说明 |
---|---|
setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程对象,并执行其它线程 |
void interrupt() | 中断线程,不建议使用这个方式 |
boolean isAlive() | 测试线程是否处于活动状态 |
3.1 状态停止
停止线程:
不推荐使用JDK提供的stop()、destroy()方法。(已废弃)
推荐线程自己停下来
建议使用一个标志位进行终止变量,当flag=false时,终止线程运行
/**
* 测试stop
* 1. 建议线程正常停止-->利用次数,不建议死循环
* 2. 建议使用标志位-->设置一个标志位
* 3. 不要使用stop或者destory等过时或者JDK不建议使用的方法
*/
public class ThreadStopDemo01 implements Runnable{
// 1. 设置一个标志位
private boolean stop = false;
@Override
public void run() {
int i = 0;
while(!stop){
System.out.println("线程执行:" + i++);
}
}
// 2. 定义一个终止线程的方法
private void stopThread(){
stop = true;
}
public static void main(String[] args) {
ThreadStopDemo01 threadStopDemo01 = new ThreadStopDemo01();
new Thread(threadStopDemo01).start();
for (int i = 0; i < 100; i++) {
System.out.println("主方法:" + i);
if(i == 85){
System.out.println("线程停止");
threadStopDemo01.stopThread();
}
}
}
}
3.2 线程休眠sleep
知识点:
sleep指定当前线程阻塞的毫秒数
sleep存在异常InterruptedException
sleep时间达到后线程进入就绪状态
sleep可以模拟网络延时,倒计时等
每一个对象都有一个锁,sleep不会释放锁
package thread.state;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.SimpleFormatter;
/**
* 模拟倒计时
*/
public class ThreadSleepDemo02 {
// 模拟倒计时 10s
public static void tenDown() throws InterruptedException {
int num = 10;
while(true){
Thread.sleep(1000);
System.out.println(num--);
if(num <= 0){
break;
}
}
}
public static void main(String[] args) {
/* try {
tenDown();
} catch (InterruptedException e) {
e.printStackTrace();
}*/
// 模拟打印系统时间
Date date = new Date(System.currentTimeMillis()); // 获取上一秒时间
while (true){
try {
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
date = new Date(System.currentTimeMillis()); // 更新上一秒时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.3 线程礼让yield
知识点:
礼让线程,让当前正在执行的线程暂停,但不要堵塞
将线程从运行状态转为就绪状态
让cpu重新调度,礼让不一定成功,看cpu
public class ThreadYieldDemo01{
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread, "a").start();
new Thread(myThread, "b").start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行");
Thread.yield(); // 礼让
System.out.println(Thread.currentThread().getName() + "线程结束执行");
}
}
a线程开始执行
b线程开始执行
a线程结束执行
b线程结束执行
3.4 线程强制执行join
/**
* 测试join方法,想象为插队
* 注意:会使线程堵塞,尽量减少使用
*/
public class JoinDemo01 implements Runnable{
public static void main(String[] args) {
// 2. 建立新线程
JoinDemo01 joinDemo01 = new JoinDemo01();
Thread thread = new Thread(joinDemo01);
thread.start();
for (int i = 0; i < 500; i++) {
if(i==200){
// 4. 插队执行
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 3. 主线程
System.out.println("主线程:" + i);
}
}
// 1. 重写run方法
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程正在运行:" + i);
}
}
}
3.5 线程状态观测
3.5.1 Thread.Stste
- 线程状态。线程可以处于下列状态之一:
NEW
至今尚未启动的线程处于这种状态。RUNNABLE
正在 Java 虚拟机中执行的线程处于这种状态。BLOCKED
受阻塞并等待某个监视器锁的线程处于这种状态。WAITING
无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。TIMED_WAITING
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。TERMINATED
已退出的线程处于这种状态。
public class TestStateDemo01 {
public static void main(String[] args) {
// 1. 新建线程
Thread thread = new Thread(()->{
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("");
}
});
// 2. 观察线程状态
Thread.State state = thread.getState();
System.out.println(state); // NEW
// 3. 启动线程
thread.start();
state = thread.getState();
System.out.println(state); // RUNNABLE
// 4. 获取运行时线程状态
while(state != Thread.State.TERMINATED){ // 只要线程不终止,就一直输出线程状态
// 5. 更新线程状态
state = thread.getState();
System.out.println(state); // TIMED_WAITING
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
NEW
RUNNABLE
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TERMINATED
3.5.2 线程优先级(priority)
java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程。(优先级高的并不总是优先执行)
优先级只意味着获得调度的概率,主要还看cpu
优先级范围:1~10
Thread.MIN_PRIOROTY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NORM_PRIORITY = 5;
获取优先级
getPriority();
改变优先级
setPriority(int val);
public class PriorityDemo01 {
public static void main(String[] args) {
// 1. 创建线程
MyPriority myPriority = new MyPriority();
Thread thread1 = new Thread(myPriority);
Thread thread2 = new Thread(myPriority);
Thread thread3 = new Thread(myPriority);
Thread thread4 = new Thread(myPriority);
Thread thread5 = new Thread(myPriority);
Thread thread6 = new Thread(myPriority);
// 2. 设置优先级,再启动
thread1.start();
thread2.setPriority(1);
thread2.start();
thread3.setPriority(3);
thread3.start();
thread4.setPriority(5);
thread4.start();
thread5.setPriority(7);
thread5.start();
thread6.setPriority(9);
thread6.start();
// main方法优先级
System.out.println(Thread.currentThread().getName() + "---->" + Thread.currentThread().getPriority());
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---->" + Thread.currentThread().getPriority());
}
}
3.5.3 守护线程(daemon)
线程分为用户线程和守护线程
虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕
如 后台记录操作日志,监控内存,垃圾回收等待
4. 线程同步(重点)
4.1 并发
并发:多个线程操作同一个对象
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这是就需要线程同步。
线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
4.2队列和锁(synchronized)
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保障数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized),当一个线程获取对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。
存在以下问题:
一个线程持有锁会导致其他所有需要此锁的线程挂起
在多线程竞争下,加锁,释放锁会导致比较多的 上下文切换 和 调度延时,引起性能问题
如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
synchronized方法控制对“对象”的调用,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronized,将会影响效率
4.2.1 synchronized同步代码块
同步块:synchronized ( Obj ) { }
Obj称之为同步监视器
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,就是这个对象本身,或者是class[反射]
同步监视器的执行过程
1. 第一个线程访问,锁定同步监视器,执行其中代码。
2. 第二个线程访问,发现同步监视器被锁定,无法访问
3. 第一个线程访问完毕,解锁同步监视器
4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
public class RunnableImpl implements Runnable {
private int tickets = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
// 同步代码块,synchronized修饰任意对象
synchronized (obj){
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + tickets + "张票");
tickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class Demo01Tickets {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
}
4.2.2 synchronized同步方法
public class RunnableImpl implements Runnable {
private int tickets = 100;
Object obj = new Object();
@Override
public void run() {
// 同步方法,默认使用this
System.out.println(this);
while (true) {
soilTicket();
if(tickets <= 0){
break;
}
}
}
// 同步方法,在方法前加 synchronized 修饰
public synchronized void soilTicket() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + tickets + "张票");
tickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo01Tickets {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
System.out.println(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
}
同步方法的其他写法:
public class RunnableImpl implements Runnable {
private static int tickets = 100;
Object obj = new Object();
@Override
public void run() {
// 同步方法,默认使用this
System.out.println(this);
while (true) {
sellTicketStatic();
if(tickets<=0){
break;
}
}
}
// 同步方法
public static /*synchronized*/ void sellTicketStatic() {
synchronized (Runnable.class){
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + tickets + "张票");
tickets--;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class RunnableImpl implements Runnable {
private static int tickets = 100;
Object obj = new Object();
@Override
public void run() {
// 同步方法,默认使用this
System.out.println(this);
while (true) {
sellTicketStatic();
}
}
private static /*synchronized*/ void sellTicketStatic() {
synchronized (Runnable.class){
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + tickets + "张票");
tickets--;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
4.3 死锁
多个线程各自占有一些共享资源,并且相互等待其它线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。某一个同步代码块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
产生死锁的必要条件
1. 互斥条件:一个资源每次只能被一个进程调用
2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不释放锁
3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
4. 循环等待条件:若干进程之间形成一种头尾相连的循环等待资源关系
只要破除其中任意一个或多个条件,就可以避免死锁的发生
// 死锁:多个线程互相抱着对方需要的资源,然后形成死锁
public class DeadLockDemo01 {
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 Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;
String girlName;
public MakeUp(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
// 化妆
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void makeup() throws InterruptedException {
if(choice == 0){
synchronized (lipstick){ // 获得口红的锁
System.out.println(this.girlName + "获得了口红");
Thread.sleep(1000);
/*
// 如果写在这里,会引起死锁
synchronized (mirror){ // 获得镜子的锁
System.out.println(this.girlName + "获得了镜子"); Thread.sleep(1000);
Thread.sleep(1000);
}
*/
}
synchronized (mirror){ // 获得镜子的锁
System.out.println(this.girlName + "获得了镜子"); Thread.sleep(1000);
Thread.sleep(1000);
}
}else{
synchronized (mirror){ // 获得镜子的锁
System.out.println(this.girlName + "获得了镜子");
Thread.sleep(2000);
/*
// 如果写在这里,会引起死锁
synchronized (lipstick){ // 获得口红的锁
System.out.println(this.girlName + "获得了口红");
Thread.sleep(2000);
}*/
}
synchronized (lipstick){ // 获得口红的锁
System.out.println(this.girlName + "获得了口红");
Thread.sleep(2000);
}
}
}
}
4.4 Lock(锁)
4.4.1 显示同步锁locks
JDK 5.0 开始,Java提供了更强大的线程同步机制---->通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks 接口是提供多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能由一个线程对Lock对象进行加锁,线程开始访问共享资源之前应先获得 Lock 对象
ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显示加锁、释放锁
class A{
private fial ReentrantLock lock = new ReentrantLock();
public void fun(){
lock.lock();
try{
// 保证线程安全的代码
}finally{
lock.unlock();
// 若同步代码块有异常,要将unlock()写入finally语句块
}
}
}
4.4.2 Lock与synchronized的对比
Lock | synchronized |
---|---|
显示锁,需要手动开关 | 隐式锁,出了作用域自动释放 |
只有代码块锁 | 有代码块锁和方法锁 |
JVM花费较少时间调度线程,性能更好。且提供了更多的子类 |
使用优先顺序:
Lock > 同步代码块(已经进来方法体,分配了相应资源) > 同步方法(在方法体之外)
4.4.3 代码
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo01 {
public static void main(String[] args) {
TestLock testLock = new TestLock();
new Thread(testLock).start();
new Thread(testLock).start();
new Thread(testLock).start();
}
}
class TestLock implements Runnable{
int tickets = 10;
// 锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
lock.lock(); // 加锁
if(tickets > 0){
System.out.println(tickets--);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}finally {
lock.unlock(); // 解锁
}
}
}
}
5. 线程通信问题
5.1 应用场景(生产者与消费者问题)
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费;
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
5.2 线程同步问题
生产者与消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
- 对于生产者:没有生产产品之前,要通知消费者等待。生产产品之后,要马上通知消费者消费
- 对于消费者:在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
- 在生产者,消费者问题上,仅有 synchronized 是不够的
- synchronized 可阻止并发更新同一个共享资源,实现了同步
- synchronized 不能用来实现不同线程之间的通信
5.3 线程通信
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其它线程通知。与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一对象上所有调用wait()方法的线程,优先级别搞得线程优先调度 |
5.4 并发协作模型
5.4.1 “生产者/消费者模式”–>管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
5.4.2 “生产者/消费者模式”–>信号灯法
6. 线程池
背景:经常创建和销毁、使用量特别大的资源,比喻并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:线程池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
使用线程池
1. JDK 5.0 起,提供了线程池相关API:ExecutorService和Executors
2. ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
1. void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnale
2. <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
3. void shutdown():关闭线程池
3. Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
|
| ------------------ | ------------------------------------------------------------ |
| wait() | 表示线程一直等待,直到其它线程通知。与sleep不同,会释放锁 |
| wait(long timeout) | 指定等待的毫秒数 |
| notify() | 唤醒一个处于等待状态的线程 |
| notifyAll() | 唤醒同一对象上所有调用wait()方法的线程,优先级别搞得线程优先调度 |
5.4 并发协作模型
5.4.1 “生产者/消费者模式”–>管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
[外链图片转存中…(img-LkwvEjmH-1606119702073)]
5.4.2 “生产者/消费者模式”–>信号灯法
6. 线程池
背景:经常创建和销毁、使用量特别大的资源,比喻并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:线程池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
使用线程池
1. JDK 5.0 起,提供了线程池相关API:ExecutorService和Executors
2. ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
1. void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnale
2. <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
3. void shutdown():关闭线程池
3. Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池