1.多线程相关概念
现在使用场景经常存在,比如“听歌、打游戏、QQ聊天同时进行”,那么怎么设计?要解决这些问题,需要使用多进程
或者多线程
来解决。
1.1 程序、进程、线程
-
程序(Program):为完成特定任务,用某种语言编写的
一组指令的集合
。即指一段静态的代码
,静态对象。 -
进程(Process)
:程序的一次执行过程,或是正在内存中运行的应用程序。如:运行中的QQ,运行中的浏览器,音乐播放器等- 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。(生命周期)
- 程序是静态的,进程是动态的
- 进程作为
操作系统调度和分配资源的最小单位
(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。 - 现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:现在我们使用电脑时一边浏览网页,一边聊天,同时还运行着 ide等软件。
-
线程(Thread)
:进程可进一步细化成线程,它是程序内部的一条执行路径
。一个进程中至少有一个线程。- 一个进程同一时间若
并行
执行多个线程,就是支持多线程的。 - 线程是
CPU调度和执行的最小单位
。 - 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使的线程间通信更简单、高效。但多个线程操作共享的系统资源可能带来
安全的隐患
。 - 下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。
- 一个进程同一时间若
注意:
不同进程之间不需要共享内存的
进程之间的数据交换和通信的成本很高
一个进程包含多个线程
核心概念:
- 线程就是独立的执行路径;
- 在程序执行时,即使没有自己创建线程,后台也会有多个线程,比如:主线程,GC线程;
- main() 称之为主线程,是系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密想关的,先后顺序是不能人为干预的;
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- 线程会带来额外的开销,比如 cpu调度时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
1.2 线程调度
1.2.1 分时调度
所有线程轮流使用
CPU的使用权,并且平均分配每个线程占用 CPU的时间。
1.2.2 抢占式调度
让优先级高
的线程以较大的概率
优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
1.3 多线程程序的优点
- 背景:以单核CPU为例,只使用单个线程先后完成多个任务(调度多个方法),肯定比用多个线程来完成的用时更短,为何仍须多线程呢?
- 多线程的优点:
①提高应用程序的响应。对图形化界面更具有意义;
②提高计算机系统 CPU的利用率;
③该善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
1.4 补充概念
1.4.1 单核CPU和多核CPU
- 单核CPU:在一个时间单元内,只能执行一个线程的任务。例如:将CPU看作医生诊室,一段时间内只能给一个病人诊断治疗。所以单核CPU就是代码经过前面一系列的前导操作(类似于医院挂号。10个挂号窗口),然后就只有一个CPU(医生),大家排队执行。
- 提升系统性能:要么提升CPU性能(医生看病快一些),要么多加几个CPU(多个医生),即为多核CPU。
- *Question:多核的效率一定比单核的倍数高吗?*如:4核A53的CPU,性能一定是单核A53的4倍吗?理论上是,但实际上不可能会,至少有两方面的损耗。
一个是多核CPU的其他共用资源限制
。如:4核CPU对应的内存、Cache、寄存器并没有同步扩充4倍。这就像虽然现在有多个医生,但是B超检查机器只有一台,性能瓶颈就从医生转到B超检查了。另一个是多核CPU之间的协调管理损耗
。如:多个核心同时运行两个相关的任务,需要考虑任务同步,这也需要消耗额外的性能。就像是在公司工作,一个人的时候不会在开会上浪费时间,两个人就要开会同步工作,协调分配,所以效率绝对不可能达到2倍。
1.4.2 并行与并发
并行(Parallel)
:指两个或多个事件在同一时刻
同时发生。指在同一时刻,有多条指令
在多个CPU
上同时
执行。比如:多个人同时做不同的事情。如下图,图 1。
并发(Concurrency)
:指两个或多个事件在同一个时间段内
发生。即在一段时间内,有多条指令
在单个CPU
上快速轮换、交替
执行,使得在宏观上具有多个进程同时执行的效果。如上图,图 2。
在操作系统中,启动了多个程序,并发
指的是在一段时间段内宏观上有多个程序同时运行,这在单核CPU系统中,每个时刻只能有一个程序执行,即微观上这些程序是分时的交替运行,只不过给人的感觉是同时运行,那是因为分时交替运行的事件是非常短的。
而在多核CPU系统中,这些可以并发
执行的程序可以分配到多个CPU上,实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场说的多核CPU,就是多核处理器,核越多,并行
处理的程序越多,能大大的提高电脑运行效率。
2.多线程创建方式
2.1 概述
- Java语言的 JVM允许程序运行多个线程,使用
java.lang.Thread
类代表线程,所有的线程对象都必须是 Thread类或是其子类的实例。 - Thread类的特性:
- 每个线程都是通过某个特定 Thread对象的 run()方法来完成操作的,因此把 run()方法体称为
线程执行体
; - 通过该 Thread对象的 start()方法来启动这个线程,而非直接调用 run();
- 要想实现多线程,必须在主线程中创建新的线程对象。
- 每个线程都是通过某个特定 Thread对象的 run()方法来完成操作的,因此把 run()方法体称为
两种方法对比:
继承 Thread类:
- 子类继承 thread类, 子类具备多线程能力
- 启动线程:子类对象.start();
- 不建议使用:避免OOP单继承局限性
实现 Runnable接口:
- 实现 Runnable接口,具有多线程能力
- 启动线程:传入目标对象+ Thread对象.start();
- 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
2.2 方式1:继承Thread类
Java通过继承 Thread类来创建并启动多线程的步骤如下
- 定义Thread类的子类;
- 重写该类的 run()方法 --> 将此线程要执行的操作,声明在此方法体中;
- 创建当前 Thread子类的实例,即创建了线程对象;
- 调用线程对象的 start()方法来启动线程:作用:①启动线程;②调用当前线程的 run()方法。
package com.mMultithreading;
/**
* 多线程创建方式1: 继承 Thread类
* 例题:创建一个分线程1,用于遍历100以内的偶数。
* 拓展:再创建一个分线程2,用于遍历100以内的偶数。
*
* 注意:t1.start()和main方法中的for循环是两个线程
*/
public class Thread1 {
public static void main(String[] args) {
//③创建当前 Thread的子类的对象
PrintNumber t1 = new PrintNumber();
//④通过对象调用 start()方法
t1.start();
//t1.start();IllegalThreadStateException
// 拓展:再创建一个分线程2,用于遍历100以内的偶数。
PrintNumber t2 = new PrintNumber();
t2.start();
//main() 所在的线程 执行的操作
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName()+": "+i);
}
}
}
}
//①创建一个继承于 Thread类的子类
class PrintNumber extends Thread {
/*②重写 Thread类的 run()方法,
将此线程要执行的操作声明在此方法中*/
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName()+": "+i);
}
}
}
}
问题一:能否使用 t1.run()替换 t1.start()的调用,实现分线程的创建和调用?
不能。
- 调用 t1.run()方法没有使用线程的方法,而是使用主线程的 run()方法,也就是说上面示例使用的是同一个线程;
- 调用 t1.start()方法的作用是:①启动线程;②调用当前线程的 run()方法。
问题二:再创建一个分线程2,用于遍历100以内的偶数。
注意:不能让已经 start()的线程再次执行 start(),否则报非法线程状态异常 IllegalThreadStateException。
PrintNumber t2 = new PrintNumber(); t2.start();
练习:创建两个分线程,其中一个线程遍历 100以内的偶数,另一个遍历 100以内的奇数。
package com.mMultithreading;
/**
* 方式一
*/
public class Thread2Test {
public static void main(String[] args) {
PrintEven t1 = new PrintEven();
PrintOdd t2 = new PrintOdd();
t1.start();
t2.start();
}
}
//输出偶数
class PrintEven extends Thread {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
//输出奇数
class PrintOdd extends Thread {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName() + ": " + i+"***");
}
}
}
}
package com.mMultithreading;
import org.junit.Test;
/**
* 方式二:创建 Thread类的匿名子类的匿名对象
*/
public class Thread2Test {
public static void main(String[] args) {
//偶数
new Thread(){
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}.start();
//奇数
new Thread(){
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
}.start();
}
}
线程是程序中执行的线程。 Java虚拟机允许应用程序同时执行多个执行线程。
每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。
2.3 方式2:实现Runnable接口
Java通过实现 Runnable接口 来创建并启动多线程的步骤如下
- 创建一个实现 Runnable接口的类;
- 实现接口中的 run()方法 --> 将此线程要执行的操作,声明在此方法体中;
- 创建当前实现类的对象;
- 将此对象作为参数传递到 Thread类的构造器中,创建 Thread的实例;
- Thread类的实例调用 start():作用:①启动线程;②调用当前线程的 run()方法
package com.mMultithreading;
/**
* 实现 Runnable接口 来创建并启动多线程
*
* 例题:创建一个分线程1,用于遍历100以内的偶数。
* 拓展:再创建一个分线程2,用于遍历100以内的偶数。
*/
public class Runnable1 {
public static void main(String[] args) {
//③创建当前实现类的对象;
EvenNumberPrint p = new EvenNumberPrint();
//④将此对象作为参数传递到 Thread类的构造器中,创建 Thread的实例;
Thread t1 = new Thread(p);
/*⑤Thread类的实例调用 start().
start()作用:①启动线程;②调用当前线程的 run()方法*/
t1.start();
//main()方法对应的主线程执行的操作
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
/*再创建一个分线程2,用于遍历100以内的偶数。*/
Thread t2 = new Thread(p);
t2.start();
}
}
/**
* ①创建一个实现 Runnable接口的类;
*/
class EvenNumberPrint implements Runnable {
/*②实现接口中的 run()方法 --> 将此线程要执行的操作,声明在此方法体中;*/
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " --> " + i);
}
}
}
}
package com.mMultithreading;
/**
* 方式三:实现 Runnable接口 的匿名子类的匿名对象
*/
public class Runnable2Test {
public static void main(String[] args) {
//偶数
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}).start();
//奇数
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " --》" + i);
}
}
}
}).start();
}
}
2.4 两种方式对比
- 共同点:
- ① 启动线程,使用的都是 Thread类中定义的 start()方法;
- ② 创建的线程对象,都是 Thread类 或其子类的实例。
- 不同点:一个是类的继承,一个是接口的实现。
- 联系:public class Thread implements Runnable (代理模式)
- 启示:建议使用
实现 Runnable接口的方式
。好处:- ① 实现的方式,避免了类的单继承的局限性;
- ② 更适合处理有共享数据的问题;
- ③ 实现了代码和数据的分离。
2.5 思考题:判断各自调用的是哪个 run()?
情景一
package com.mMultithreading;
/** 5
* 思考题:判断各自调用的是哪个 run()?
*/
public class Test {
public static void main(String[] args) {
A a = new A();
a.start();//①启动线程;②调用 Thread类的 run()r方法
B b = new B(a);
b.start();
}
}
//创建线程类 A
class A extends Thread{
@Override
public void run() {
System.out.println("线程 A的--> run()");
}
}
//创建线程类 B
class B extends Thread{
private A a;
/*构造器中直接传入 A类对象。下面两种方法结果相同
public B(A a) {this.a = a;}*/
public B(A a) {super(a);}
/**
* 注意:当这里没有重写 run()方法,那么就会调用 A类中的 run()方法
* 前提:上面的构造器要使用 super(a)的这个
*/
@Override
public void run() {
System.out.println("线程 B的--> run()");
}
}
情景二
package com.mMultithreading;
/** 6
* 判断各自调用的是哪个 run()?
*/
public class Test2 {
public static void main(String[] args) {
BB b = new BB();
//调用的就是 形参对象的 run()方法
new Thread(b){}.start();//BB
//这里使用的就是 自己的 run()方法
new Thread(b){
@Override
public void run() {
System.out.println("CC");//CC
}
}.start();
}
}
class AA extends Thread{
@Override
public void run() {
System.out.println("AA");
}
}
class BB implements Runnable{
@Override
public void run() {
System.out.println("BB");
}
}
3.Thread类的常用结构
3.1 构造器
//分配一个新的线程对象
public Thread();
//分配一个指定名字的新线程对象
public Thread(String name);
//指定创建线程的目标对象,它实现了 Runnable接口中的 Run()方法
public Thread(Runnable target);
//分配一个带有指定目标的新线程对象,并指定名字
public Thread(Runnable target, String name);
package com.mMultithreading;
/**
* 线程中的构造器
*/
public class Test3Construct {
public static void main(String[] args) {
EvenNumber p1 = new EvenNumber("线程_1:");
p1.start();
EvenNumber p2 = new EvenNumber(p1,"线程_2:");
p2.start();
}
}
class EvenNumber extends Thread{
public EvenNumber() {}
public EvenNumber(String name) {super(name);}
public EvenNumber(Runnable target, String name) {super(target, name);}
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i%2 ==0){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}
3.2 常用方法_系列1
public void run();//此线程要执行的任务,需要在此处定义代码。
public void start();//开始执行此线程;Java虚拟机调用此线程的 run()方法
public String getName();//获取当前线程的名称
public void setName(String name);//设置当前线程的名称
public static native Thread currentThread();//返回当前正在执行的线程对象的引用。在Thread子类中就是 this,通常用于主线程和 Runnable实现类
public static void sleep(long millis);//是当前正在运行的线程暂停 millis毫秒(暂时停止执行)
public static native void yield();//yield只是让当前线程暂停一下,让系统的线程调度器从新调度一次,希望优先级
3.3 常用方法_系列2
- public final boolean isAlive();//判断当前线程是否存活
- void join(); 等待该线程终止
void join(long millis); 等待该线程终止的时间最长是 millis毫秒。时间到,就不再等待
void join(long millis, int nanos); 等待该线程终止的时间最长是 millis毫秒 +nanos纳秒 - public final void stop();
已过时
,不建议使用。强行结束一个线程的执行,直接进入死亡状态。run()方法即刻停止可能会导致一些清理性的工作不能完成,如文件、数据库的关闭等,同时会立刻释放掉该线程持有的所有锁,导致数据不能同步,出现数据不一致的问题。 - void suspend() / void resume();
已过时
不建议使用。这两个操作好比播放器的暂停和恢复,二者必须成对出现,否则非常容易发生死锁。调用suspend()会导致线程暂停,但不会释放掉任何锁资源,导致其他线程都无法访问被它占用的锁,直到调用resume()。
package com.mMultithreading;
public class Test4Method {
public static void main(String[] args) {
PrintNumberTest t1 = new PrintNumberTest("线程_1:");
//设置线程名称
t1.setName("子线程1:");
//①启动线程 ②调用线程的run()
t1.start();
Thread.currentThread().setName("主线程");
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
//getName():获取线程名称
System.out.println(Thread.currentThread().getName() + "--> " + i);
}
if (i % 20 == 0) {
try {
//在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,
// 直到线程b执行结束,线程a才结束阻塞状态,继续执行。
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//isAlive():判断当前线程是否存活
System.out.println("子线程1是否还存活?" + t1.isAlive());
}
}
class PrintNumberTest extends Thread {
public PrintNumberTest() {
}
public PrintNumberTest(String name) {
super(name);
}
/**
* 此线程要执行的任务,需要在此处定义代码。
*/
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
try {
//静态方法,调用时,调用时可以使当前线程睡眠指定的毫秒数
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i % 2 == 0) {
//currentThread():获取当前执行代码对应的线程
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
if (i % 20 == 0) {
//一旦执行此方法,就释放CPU的执行权
Thread.yield();
}
}
}
}
3.4 线程的优先级
每个线程都有一定的优先级,同优先级线程组成先进先出队列(FIFO:先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
- Thread内部类声明的三个优先级常量:
- MIN_PRIORITY (1) :最低优先级
- NORM_PRIORITY (5) :普通优先级,默认情况下 main线程具有普通优先级。
- MAX_PRIORITY (10) :最高优先级
- public final int getPriority():获取线程的优先级
- public final int setPriority(int newPriority):改变线程的优先级,范围在
[1, 10]
之间。
package com.mMultithreading;
/**
* 线程方法_3:
* getPriority():获取线程的优先级
*/
public class Test5Method {
public static void main(String[] args) {
PrintNumberTest5 t1 = new PrintNumberTest5("线程_1:");
t1.setPriority(Thread.MIN_PRIORITY);
t1.start();
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
//getName():获取线程名称
System.out.println(Thread.currentThread().getName() + ":" +
//getPriority():获取线程优先级
Thread.currentThread().getPriority()+ i);
}
}
}
}
class PrintNumberTest5 extends Thread {
public PrintNumberTest5(String name) {
super(name);
}
/**
* 此线程要执行的任务,需要在此处定义代码。
*/
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" +
//getPriority():获取线程优先级
Thread.currentThread().getPriority()+ i);
}
}
}
}
4.多线程的生命周期
Java语言使用 Thread类及其子类的对象来表示线程,在它的一个完整生命周期内通常要经历一下状态
4.1 JDK1.5 之前:5种状态
它们分别是:新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程会多次在运行、阻塞、就绪之间切换
4.1 JDK1.5 及之后:6种状态
5.线程安全问题及解决
当我们使用多个线程访问同一资源
(可以是同一个变量、同一个文件、同一条记录)的时候,若多个线程只有读操作
,那么不会发生线程安全问题。但是如果多个线程对资源有读和写
的操作,就容易出现线程安全问题。
举例:
对一个账户进行取钱操作;
5.1统一资源问题和线程安全问题
案例:
模拟火车站卖票的过程。如果本次列车的座位共100个(即只能出售100张火车票)。这里模拟售票窗口,实现多个窗口同时售票的过程。注意:不能出现错票、重票。
package com.mMultithreading.synBlock;
/** 出现了错票、重票,同步代码块和同步方法中的代码解决了这些问题!!!
* 模拟车站售票:模拟车站售票:实现Runnable接口方式,实现卖票
* 3个窗口,共100张票
*/
class SaleTicket implements Runnable {
static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
ticket--;
} else {
break;
}
}
}
}
public class Test1WindowIM {
public static void main(String[] args) {
SaleTicket s = new SaleTicket();
Thread t1 = new Thread(s,"售票窗口1");
Thread t2 = new Thread(s,"售票窗口2");
Thread t3 = new Thread(s,"售票窗口3");
t1.start();
t2.start();
t3.start();
}
}
-
多线程卖票,出现的问题:
出现了重票和错票 -
什么原因导致的?
线程1操作 ticket 时,其他进程也参与进来,并对 ticket 进行操作。 -
解决思路?
必须保证一个线程a在操作 ticket 时,其他线程必须等待,直到线程a操作ticket结束后,其他线程才可以进入操作 ticket。 -
Java如何解决线程的安全问题?
使用线程的同步机制。① 同步代码块;② 同步方法
5.2 同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
共享数据
:即多个线程需要操作的数据。比如:ticket
需要被同步的代码
,即为操作共享数据的代码,它被synchronized包裹以后,就使得一个线程在操作这些代码的过程时,其他线程必须等待。
同步监视器
,俗称锁
。哪个线程获得了锁,哪个线程就能执行需要被同步的代码;它可以使用任何一个类的对象充当。但是,多个线程必须保证同步监视器唯一
。
package com.mMultithreading.synBlock;
/**
* 模拟车站售票:实现Runnable接口方式,实现卖票
* 使用同步代码块解决买票中的线程安全问题。
* 3个窗口,共100张票
*/
class Dog {
}
class Sale implements Runnable {
static int ticket = 100;
Object obj = new Object();
Dog dog = new Dog();
@Override
public void run() {
// synchronized (this) {//当前类是唯一的?yes,就是题中的s
while (true) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// synchronized (obj){//obj是唯一的?yes
// synchronized (dog) {//dog是唯一的?yes
synchronized (this) {//当前类是唯一的?yes,就是题中的
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class Test3WindowIM {
public static void main(String[] args) {
Sale s = new Sale();
Thread t1 = new Thread(s, "售票窗口1");
Thread t2 = new Thread(s, "售票窗口2");
Thread t3 = new Thread(s, "售票窗口3");
t1.start();
t2.start();
t3.start();
}
}
package com.mMultithreading.synBlock;
/**
* 模拟车站售票:继承Thread类方式
* 使用同步代码块解决买票中的线程安全问题。
* 3个窗口,共100张票
*/
class WatchDog {}
class Ticket extends Thread {
static int ticket = 100;
WatchDog dog = new WatchDog();
static Object obj=new Object();
@Override
public void run() {
while (true) {
// synchronized (this) {//this此时表示t1,t2,t3
// synchronized (obj) {//static 修饰后就能保证使其唯一
// synchronized (dog) {
synchronized (Ticket.class) {//反射结构:Class cla = Ticket.class;类只加载一次所以它是唯一的。
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class Test4WindowEX {
public static void main(String[] args) {
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
t1.setName("售票窗口1");
t2.setName("售票窗口2");
t3.setName("售票窗口3");
t1.start();
t2.start();
t3.start();
}
}
- 注意:
- 在实现Runnable接口的方式中,同步监视器可以考虑使用:this ;
- 在继承Thread类的方式中,同步监视器 慎用 this,可以考虑使用:当前类.class 。
5.3 同步方法
- 如果操作共享数据的代码完整的声明在了一个方法中,那么就可以将此方法声名为同步方法即可。
- 非静态的同步方法,默认同步监视器是 this(要考虑是否唯一?);
- 静态的同步方法,默认同步监视器是当前类本身。
5.3.1 非静态方法加锁
package com.mMultithreading.synMethod;
/**
* 模拟车站售票:实现Runnable接口方式,实现卖票
* 使用同步方法解决买票中的线程安全问题。
* 3个窗口,共100张票
*/
class Sale implements Runnable {
static int ticket = 100;
static boolean flag = true;
@Override
public void run() {
while (flag) {
show();
}
}
/**
* 方法由synchronized修饰后就变成了非静态同步方法
* 此时的同步监视器默认是:this。此题目中即为`s`,它是唯一的。
*/
public synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
ticket--;
} else {
flag = false;
}
}
}
public class Test5WindowIM {
public static void main(String[] args) {
Sale s = new Sale();
Thread t1 = new Thread(s, "售票窗口1");
Thread t2 = new Thread(s, "售票窗口2");
Thread t3 = new Thread(s, "售票窗口3");
t1.start();
t2.start();
t3.start();
}
}
5.3.2 静态方法加锁
package com.mMultithreading.synMethod;
/**
* 模拟车站售票:继承Thread类方式
* 使用同步方法解决买票中的线程安全问题。
* 3个窗口,共100张票
*/
class Ticket extends Thread {
static int ticket = 100;
static boolean flag = true;
@Override
public void run() {
while (flag) {
show();
}
}
/**
* 方法由synchronized修饰后就变成了静态同步方法
* 此时的同步监视器默认是:Ticket.class(是一个对象),它是唯一的。
*/
//public synchronized void show(){此时的同步监视器默认是:this。此题目中即为`t1,t2,t3`,它不是唯一的。
public static synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
ticket--;
} else {
flag = false;
}
}
}
public class Test6WindowEX {
public static void main(String[] args) {
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
t1.setName("售票窗口1");
t2.setName("售票窗口2");
t3.setName("售票窗口3");
t1.start();
t2.start();
t3.start();
}
}
5.4 synchronized优弊
- 优点:
- 解决了现成的安全问题。
- 弊端:
- 在操作共享数据时,多线程其实是串行执行的,性能低。
5.5 练习
甲乙两人往一个账户各存3000元,分三次,每次存完输出余额,是否有线程安全问题?
package com.mMultithreading;
/**
* 账户
*/
class Account {
//余额
private double balance;
/**
* @param amt 存款额
* @description 存款。这里默认是this,本题中是acc,故是唯一
*/
public synchronized void deposit(double amt) {
if (amt > 0) {
balance += amt;
}
System.out.println(Thread.currentThread().getName() +
"存款" + amt + "元,余额为:" + balance);
}
}
class Customer extends Thread {
Account account;
public Customer(Account acct) {
this.account = acct;
}
public Customer(Account acct, String name) {
super(name);
this.account = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.deposit(1000);
}
}
}
public class Test3Account {
public static void main(String[] args) {
Account acc = new Account();
Customer c1 = new Customer(acc, "甲");
Customer c2 = new Customer(acc, "乙");
c1.start();
c2.start();
}
}
6.线程同步
线程安全的懒汉式、死锁
6.1单例模式之懒汉式的线程安全问题
饿汉式:不存在线程安全问题;
懒汉式:存在线程安全问题,需要使用同步机制来处理。
注意:方法3中,有指令重排问题
。
- men = allocate(); 给单例对象分配内存空间
- instance = mem; instance引用现在非空,但还未初始化
- ctorSingleton( instance); 单例对象通过instance调用构造器
- 从JDK2开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要
volatile
关键字,避免指令重排。
package com.mMultithreadOther;
/**
* 实现线程安全的 懒汉式
*/
class Bank { //银行
private Bank() {
}
//instance:共享数据,地址值
//volatile关键字作用:避免指令重排
private static volatile Bank instance = null;
//是先线程安全方式1:同步监视器,默认为Bank.class
/*public static synchronized Bank getInstance() {
if (instance == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Bank();
}
return instance;
}*/
//是先线程安全方式2:同步监视器,默认为Bank.class
/*public static Bank getInstance() {
//synchronized (this) {静态方法不能使用this
synchronized (Bank.class) {
if (instance == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Bank();
}
}
return instance;
}*/
/*是先线程安全方式3:相较于方式1和2,相率更高一些
为了避免指令重排,将 instance 声明为 volatile */
public static Bank getInstance() {
if (instance==null){
synchronized (Bank.class) {
if (instance == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Bank();
}
}
}
return instance;
}
}
public class SingletonLazy {
static Bank b1 = null;
static Bank b2 = null;
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
b1 = Bank.getInstance();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
b2 = Bank.getInstance();
}
};
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(b1);
System.out.println(b2);
System.out.println(b1 == b2);
}
}
6.2 同步机制带来的问题:死锁
-
如何为看待死锁?
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
一旦出现死锁,整个程序既不会发生异常,也不会给出提示,只是所有线程处于阻塞状态,无法继续。 -
出现死锁的原因?
以下四个条件,同时出现就会出现死锁
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待
- 如何避免死锁?
死锁一旦出现,基本很难人为干预,只能尽量规避,可以打破任意一个条件。为保证同步需要,只能对后面 3个条件进行破坏。
- 针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题;
- 针对条件2:可以考虑
一次申请所有需要的资源
,就不会出现等待的问题; - 针对条件3:占用部分资源的线程,进一步申请其他资源时,若申请不到,就
主动释放掉已占用的资源
; - 针对条件4:可以
将资源改为线性顺序
。申请资源时,先申请序号较小的,这样就避免了循环等待的问题
6.3.JDK5 Lock锁的使用
- java.util.concurrent.locks(简称JUC,并发编程使用的包)
- JDK5.0的新增功能,保证线程的安全。与采用synchronized相比,Lock可以提供多种锁方案,更灵活、更强大。Lock通过显式定义同步锁对象实现同步。同步锁使用Lock对象充当
- 面试题:synchronized同步的方式,与Lock的对比?
- synchronized不管是同步代码块还是同步方法,都需要在结束一对“{}”之后,释放对同步监视器的调用;
- Lock是通过调用两个方法控制需要被同步的代码,更灵活。
- Lock作为接口,提供了多种实现类,适合更多复杂的场景,效率更高。
- 使用步骤:
① 创建Lock的实例,需要确保多个线程共用一个Lock实例。这里是继承Thread,需要考虑使用static修饰;
② 执行Lock方法,将共享资源加锁;
③ unlock()的调用,将共享资源释放
package com.mMultithreadOther;
import java.util.concurrent.locks.ReentrantLock;
/**
* java.util.concurrent.locks
* 模拟车站售票:实现Runnable接口方式,实现卖票
* 使用 JDK1.5Lock(锁)解决买票中的线程安全问题。
* 3个窗口,共100张票
*/
class Sale extends Thread {
static int ticket = 100;
//1.创建Lock的实例,需要确保多个线程共用一个Lock实例。这里是继承Thread,需要考虑使用static修饰
private static final ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//2.执行Lock方法,将共享资源加锁
reentrantLock.lock();
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
ticket--;
} else {
break;
}
} finally {
//3.unlock()的调用,将共享资源释放
reentrantLock.unlock();
}
}
}
}
public class Test4Lock {
public static void main(String[] args) {
Sale s1 = new Sale();
Sale s2 = new Sale();
Sale s3 = new Sale();
s1.setName("窗口1");
s2.setName("窗口2");
s3.setName("窗口3");
s1.start();
s2.start();
s3.start();
}
}