第8章 多线程
1、基本概念:程序、进程、线程
程序(program): 是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process): 是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:由它自身的产生、存在和消亡的过程,即生命周期。
1.当我们打开QQ或者视频播放器后,运行的QQ和视频播放器就是一个进程。
2. 程序是静态的,进程是动态的。
3. 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程(thread): 进程可进一步细化为线程,是一个程序内部的一条执行路径(程序执行过程,可以用一条线连起来的即为一条执行路径)。
1. 若一个进程同一时间并行执行多个线程,就是支持多线程的。
2. 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
3. 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效(内存区域中的虚拟机栈和程序计数器对于每一个线程都有属于自己的一份,对于内存区域中的方法区和堆对于每一个进程都有属于自己的一份,进程中有多个线程,即多个线程共享一个进程中的方法区和堆)。但多个线程操作共享的系统资源可能就会带来安全隐患。
其他知识要点:
1.单核CPU和多核CPU的理解:
单核CPU其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。但由于CPU的主频比较高,时间单元特别短,因此,多个线程轮流依次执行无法被感觉出来(感觉是并发运行)。
如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核)
对于一个Java的应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程(如何程序运行中发生异常,会影响主线程)。
2.并行和并发:
并行:多个CPU同时执行多个任务。
并发:一个CPU(采用时间片)同时执行多个任务。
3.使用多线程的优点:
对于单核CPU来说,只使用单个线程先后完成多个任务的速度可定是比多个线程完成多个任务的速度更快(因为对于多个线程完成多个任务,需要考虑一个CPU切换不同线程时候的时间等)
但多线程程序能够提高应用程序的响应,对图形化界面更有意义,可增强用户体验;提高计算机系统CPU的利用率;改善程序结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改(不同功能分成不同的模块)。
4.何时需要多线程:
(1)程序需要同时执行两个或多个任务。
(2)程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
(3)需要一些后台运行的程序时。
2、线程的创建和使用
2.1 引出:
Java语言的JVM允许程序运行多个线程(并发),它通过java.lang.Thread类来体现。
2.2 Thread的特性:
-
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体。
-
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()。
2.3 具体的多线程创建以及使用例子:
方法一:通过继承与Thread类实现多线程
package com.java;
/**
* 多线程的创建,方式一:继承于Thread类
* 步骤:
* 1.创建一个继承于Thread类的子类
* 2.重写Thread类中的run方法 --> 将此线程执行的操作声明在run()方法中
* 3.创建Thread类的子类的对象
* 4.通过此对象调用start()方法
* start作用: 1、启动当前线程 2、调用当前线程的run()方法(因为当前线程重写了父类中的run方法)
*
*
* 例子:遍历100以内的所有的偶数
* @author banana
* @create 2023-02-15 19:18
*/
public class ThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子类的对象
MyThread t1 = new MyThread();
//4.调用start()方法
//问题一:我们不能通过直接调用run方法去启动线程
//如果直接a.run表示调用MyThread类对象a的run方法,并没有开启新的线程,还是在main线程中执行
t1.start();
//问题二:再启动一个线程,遍历100以内的偶数
//不可以还让已经start()的线程去执行,会报异常IllegalThreadStateException
//t1.start();
MyThread t2 = new MyThread();
t2.start();
//如下的操作仍然是在main线程中执行的
for(int i = 1; i <= 100; i ++){
if(i % 2 == 1)System.out.println("*" + i);
}
}
}
//1.创建一个继承与Thread类的子类
class MyThread extends Thread{
//2.重写Thread类中的run方法
@Override
public void run() {
for(int i = 1; i <= 100; i ++){
if(i % 2 == 0)System.out.println(i);
}
}
}
package com.java;
/**
* 较上一个例子,我们可以通过创建Thread类的匿名子类的方式去实现
* 例子:遍历100以内的所有的偶数
* @author banana
* @create 2023-02-15 19:18
*/
public class ThreadTest {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for(int i = 0; i <= 100; i ++){
if(i % 2 == 1) System.out.println("*" + i);
}
}
}.start();
}
}
方法二:通过实现Runnable接口的方式来实现多线程
package com.java;
/**
* 创建多线程的方式二:实现Runnable接口
* 具体操作:
* 1.创建一个实现Runable接口的类
* 2.实现类去实现Runnable中的抽象方法:run()
* 3.创建实现类的对象
* 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
* 5.通过Thread类的对象调用start();
*
* @author banana
* @create 2023-02-16 8:52
*/
//1.创建一个实现Runable接口的类
class MyThread2 implements Runnable{
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for(int i = 0; i < 100; i ++){
if(i % 2 == 0){
//这里不能使用this.getName()
//因为MyThread2不是继承Thread,是继承与Object,没有对应的方法
//因此我们要通过THread中的静态方法currentThread先获取当前的线程对象
//然后再通过当前的线程对象去调用Thread类中的getName方法
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
//3.创建实现类的对象
MyThread2 myThread2 = new MyThread2();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread thread = new Thread(myThread2);
//5.通过Thread类的对象调用start(); 1.启动线程 2.调用当前线程的run()
/*
对于疑问这里调用的是Thread的run方法,而不是Runnable对象的run方法,为什么第二点说是调用当前线程的run方法的解释:
对于run的源码,根据放入构造器的Runnable对象赋值给Thread类中的target
因此当前线程的run调用了Runnable类型的target的run方法
public void run() {
if (target != null) {
target.run();
}
}
*/
thread.setName("线程1");
thread.start();
//再启动一个线程,遍历100以内的偶数
Thread thread2 = new Thread(myThread2);
thread2.setName("线程2");
thread2.start();
}
}
对两者创建多线程方式的比较:
开发中,优先选择:实现Runnable接口的方式来创建多线程方法。
原因:
-
实现的方式没有类的单继承的局限性,即如果某个类继承了Thread,就不能在继承其他类了。
-
实现的方式更适合来处理多个线程有共享数据的情况。(因为可以把多个线程共享的数据封装在实现Runnable接口的类当中,然后类的对象作为参数传到多个线程的构造器当中)
例子:
package com.java; /** * 例子:创建三个窗口卖票,总票数为100张 * 使用继承Thread方式实现 * * 存在线程的安全问题(待解决) * @author banana * @create 2023-02-16 8:40 */ public class WindowsTest { public static void main(String[] args) { Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); w1.setName("窗口1"); w2.setName("窗口2"); w3.setName("窗口3"); w1.start(); w2.start(); w3.start(); } } class Window extends Thread{ private static int ticket = 100; //static:使得三个对象公用一份票 @Override public void run() { while(true){ //还有余票 if(ticket > 0){ System.out.println(Thread.currentThread().getName() + ": 卖票的票号为" + ticket); ticket --; } else break; } } } package com.java; /** * 例子:创建三个窗口卖票,总票数为100张 * 通过使用Runnable接口的方式实现 * @author banana * @create 2023-02-16 9:20 */ public class WindowsTest2 { public static void main(String[] args) { Window2 window2 = new Window2(); Thread t1 = new Thread(window2); Thread t2 = new Thread(window2); Thread t3 = new Thread(window2); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } class Window2 implements Runnable{ //为什么这里不用加static,也能使得之后的三个线程t1,t2,t3公用这100张票? //因为对于三个线程都是new的同一个window2,因此线程中的ticket也是同一个。 private int ticket = 100; @Override public void run() { while(true){ if(ticket > 0){ System.out.println(Thread.currentThread().getName() + ": 票号为:" + ticket); ticket --; } else { break; } } } }
联系:
- Thread类本身也是实现了Runnable接口,源码:
public class Thread implements Runnable
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()方法中;目前这两种方式要启动线程,都是调用Thread类中的start方法。
2.4 Thread的方法
package com.java;
/**
* 测试Thread中的常用方法
* 1. start():启动当前线程,调用当前线程的run()方法
* 2. run(): 通常需要重写Thread类中的此方法,将创建的线程执行的操作声明在此方法中
* 3. currentThread(): 【静态方法】返回执行当前代码的线程(返回类型为Thread)
* 4. getName():获取当前线程的名字
* 5. setName():设置当前线程的名字
* 6. yield(): 释放当前cpu的执行权
* 7. join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,
* 线程a才结束阻塞状态。
* 8.stop():强制线程生命期结束(不推荐使用,已过时)
* 9.sleep(long millitime): 让当前线程“睡眠”指定的millitime毫秒数,在指定的millitime毫秒
* 时间内,当前线程是阻塞状态。
* 10.isAlive() : 判断当前线程是否存活(run方法内容是否执行完毕)
*
*
* @author banana
* @create 2023-02-15 20:26
*/
class Th extends Thread{
@Override
public void run() {
for(int i = 0; i <= 100; i ++){
if(i % 2 == 0){
try {
sleep(10); //当前线程阻塞1s
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//if(i % 20 == 0)this.yield();
}
}
//构造器:
public Th(String name){
super(name);
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
//通过线程的构造器为线程命名
Th h1 = new Th("线程1");
//h1.setName("线程1");
h1.start();
//通过setName方法给主线程进行命名:
Thread.currentThread().setName("主线程");
for(int i = 0; i <= 100; i ++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
//主线程执行到20时,h1调用join方法,使得该分线程参与进来
//当分线程执行完之后,主线程才开始执行
if(i == 20){
try {
h1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(h1.isAlive());
}
}
2.5 线程的调度
2.5.1 调度的策略:
(1)时间片
(2)抢占式:根据优先级进行抢占,高优先级的线程抢占CPU
2.5.2 Java的调度方法:
同优先级线程组成先进先出队列(先到先服务),使用时间片策略。
对高优先级使用优先调度的抢占式策略。
2.5.3 线程的优先级等级:
MAX _PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5(默认)
涉及的方法:
getPriorty(): 返回线程优先值
setPriority(int newPriority):改变线程的优先级
例子:
package com.java;
/**
*
* 1.线程的优先级:
* MAX _PRIORITY:10
* MIN_PRIORITY:1
* NORM_PRIORITY:5(默认)
*
* 2.如何获取和设置当前线程的优先级:
* getPriorty(): 返回线程优先值
* setPriority(int newPriority):改变线程的优先级
*
* @author banana
* @create 2023-02-15 21:33
*/
public class ThreadPriority {
public static void main(String[] args) {
Thr thr = new Thr();
thr.setPriority(Thread.MAX_PRIORITY);
thr.start();
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for(int i = 0; i <= 100; i ++) {
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
}
}
}
class Thr extends Thread{
@Override
public void run() {
for(int i = 0; i <= 100; i ++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
}
}
}
说明:
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
2.6 线程的分类
Java中的线程分为两类:一种是守护线程,一种是用户线程。
-
他们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
-
守护线程是用来服务用户线程的,通过start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程
-
Java垃圾回收就是一个典型的守护线程。
-
若JVM中都是守护线程,当前JVM讲退出。
3、线程的生命周期
3.1 线程的状态
JDK中用Thread.State类定义了线程的几种状态,要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来实现多线程,在它的一个完整的声明周期中通常要经历如下的五中状态:
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
- 阻塞:在某种特殊情况下,被人挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞钻头。
- 死亡:线程完成了它的全部工作或线程被提前强制地中止或出现异常导致结束。
4、线程的同步
4.1 问题的提出
线程的安全问题:
-
多个线程执行的不确定性会引起执行结果的不稳定。
-
多个线程对数据的共享(即有共享数据),会造成操作的不完整性,会破坏数据。
线程安全问题的举例:如有一个多线程的共享数据是银行中的存款3000元,现在有两个线程分别是a和b,他们分别打算去银行中取该存款中的2000元。当a完成if判断语句,判断当前要取的钱2000小于银行中的实际存款数3000,即表示可以取,进入if语句。当出现一种极端的情况时,即当a进入if语句的同时因为一些原因被阻塞,还没有对共享数据的3000进行减去2000的操作,此时b也进行if语句的判断,此时也满足取钱要求,其也会进入if语句,那么最后的结果就是a和b都从该银行的存款中中取走2000,导致,银行中的存款变成-1000,这显然是不合理的。这就导致了线程的安全问题。
4.2 线程的同步的处理方法:解决线程安全问题
一、对使用Runnable接口的方式实现多线程解决同步:
方法一、同步代码块
package program;
/**
* 例子:创建三个窗口卖票,总票数为100张
* 通过使用Runnable接口的方式实现
*
*
* 存在线程的安全问题:
* 1.卖票过程中出现了重票(出现相同的票)
* 2.卖票过程中出现了错票(出现票号为0和-1)
*
* 问题出现的原因:
* 总的来说,是由于当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
* 1.线程1在还未执行ticket--的时候,线程2就进来执行了System.out.println(Thread.cur
* rentThread().getName() + ": 卖票的票号为" + ticket);则导致了最终线程1和线程2抢到的票
* 是一样的,这就导致了卖票过程中出现了重票。
* 2.假设当前票数为当线程1通过if判断语句进入后的同时,线程2也通过if判断语句进入,并且线程3也
* 通过if判断语句进入,之后cpu依次执行线程1、线程2、线程3的run中的if语句中的内容,导致线程1
* 抢到的票为1,线程2抢到的票为0,线程3抢到的票为-1,这样即导致了卖票过程中错票的出现。
*
*
* 解决方法(广义):
* 当一个线程a在操作ticket的时候,其他线程不能参与进来,直到线程a操作完毕,其他线程才可以参与进来。
* 这种情况,即是线程a出现了阻塞,也不能被改变。
*
* 在Java中的具体解决方法:通过同步机制,来解决线程的安全问题。
* 方法一、同步代码块
* 关键字:synchronized(同步监视器){
* //需要被同步的代码……
* }
* 说明:
* 1.需要同步的代码:操作共享数据的代码。
* 2.共享数据:多个线程共同操作的变量。比如ticket就是共享数据。
* 3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
* 要求:多个线程必须要共用同一把锁。
* 补充:在实现Runnable接口的创建多线程的方式中,我们可以考虑使用使用this充当同步监视器。
* 方法二、同步方法
* 如果操作共享数据的代码完整地声明在一个方法中,我们不妨将此方法声明为同步的。
* @author banana
* @create 2023-02-16 9:20
*/
public class WindowsTest2 {
public static void main(String[] args) {
Window2 window2 = new Window2();
Thread t1 = new Thread(window2);
Thread t2 = new Thread(window2);
Thread t3 = new Thread(window2);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window2 implements Runnable{
//为什么这里不用加static,也能使得之后的三个线程t1,t2,t3公用这100张票?
//因为对于三个线程都是new的同一个window2,因此线程中的ticket也是同一个。
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while(true){
//synchronized(this):此时的this:唯一的window对象
synchronized(obj){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ": 票号为:" + ticket);
ticket --;
}
else {
break;
}
}
}
}
}
方法二、同步方法
package program;
/**
*
* 使用同步方法解决实现Runnable接口的线程安全问题
*
* 关于同步方法的总结:
* 1.同步方法仍然设计到同步监视器,只是不需要显示地声明。
* 2.非静态的同步方法,同步监视器是:this
* 3.静态的同步方法,同步监视器是:当前类本身(也属于对象)
*
* @author banana
* @create 2023-02-16 9:20
*/
public class WindowsTest4 {
public static void main(String[] args) {
Window4 window4 = new Window4();
Thread t1 = new Thread(window4);
Thread t2 = new Thread(window4);
Thread t3 = new Thread(window4);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window4 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
show();
}
}
// 同步方法
//同步监视器:this
private synchronized void show(){
if(ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 票号为:" + ticket);
ticket--;
}
}
}
补充1:
1.对于错票的图示解释:
2.对于使用synchronized的同步原理的解释:
首先,图中的obj是监视器(锁)。当我们的线程t1抢到监视器obj后,obj就会被占用,其他线程就无法再进入,直到t1线程完成了对tick的所有操作后,才释放监视器obj,之后线程t1、t2、t3又可以重新对监视器进行抢占,所抢到谁进行执行操作。(这里t1在释放监视器后,仍然有可能再次抢占到监视器)。
3.对于尽管使用了synchronized,但不是同一把锁,仍然造成线程安全问题的代码举例:
package program;
/**
* 例子:创建三个窗口卖票,总票数为100张
* 使用继承Thread方式实现
* @author banana
* @create 2023-02-16 8:40
*/
public class WindowsTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread{
private static int ticket = 100; //static:使得三个对象公用一份票
Object obj = new Object(); //错误,没有共有同一把锁,三个对象三把锁
@Override
public void run() {
while(true){
//synchronized(this):错误,此时this代表w1,w2,w3三个对象
synchronized(obj){
//还有余票
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 卖票的票号为" + ticket);
ticket --;
}
else break;
}
}
}
}
二、对使用继承Thread的方式实现多线程解决同步:
方法一、使用同步代码块
package program;
/**
* 例子:创建三个窗口卖票,总票数为100张
* 使用继承Thread方式实现
*
* 方法一、使用同步代码块
*
* 补充:在继承Thread类创建多线程的方式中,慎用this充当同步监视器(一般情况下,this不唯一)
* 可以考虑当前类window.class充当同步监视器。
* @author banana
* @create 2023-02-16 8:40
*/
public class WindowsTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread{
private static int ticket = 100; //static:使得三个对象公用一份票
private static Object obj = new Object(); //static:使得三个线程公用一把锁
@Override
public void run() {
while(true){
//synchronized(window.class):此时的windows.class是window的唯一对象,类本身也充当一个对象
synchronized(obj){
//还有余票
if(ticket > 0){
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName() + ": 卖票的票号为" + ticket);
ticket --;
}
else break;
}
}
}
}
方法二、同步方法
package program;
/**
* 方式二、使用同步方法继承Thread类的方式中的线程安全问题
*
* @author banana
* @create 2023-02-16 8:40
*/
public class WindowsTest3 {
public static void main(String[] args) {
Window3 w1 = new Window3();
Window3 w2 = new Window3();
Window3 w3 = new Window3();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class Window3 extends Thread{
private static int ticket = 100; //static:使得三个对象公用一份票
@Override
public void run() {
while(true){
show();
}
}
//public synchronized void show()同步监视器:this(t1、t2、t3)仍然是线程不安全的
//因此我们要将方法写成静态的
//public static synchronized void show() 同步监视器:Window3.class
public static synchronized void show(){
//还有余票
if(ticket > 0) {
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName() + ": 卖票的票号为" + ticket);
ticket--;
}
}
}
补充2:
1、使用同步机制,将单例模式中的懒汉式改写成为线程安全的单例模式
原单例模式展示,以及其存在的问题分析:
package Program1;
/**
* 使用同步机制,将单例模式中的懒汉式改写为线程安全的
* @author banana
* @create 2023-02-16 15:29
*/
public class BankTest {
}
//懒汉式的单例实现模式
class Bank{
private Bank(){
}
private static Bank bank = null;
//当多个线程调用Bank的getBank方法的时候
//可能会造成多次创建对象的情况出现,这样是线程不安全的
//即线程1得到的Bank实例对象的地址不等于线程2得到的Bank实例的对象地址
public static Bank getBank(){
//对该if语句的抢占可能最终导致线程的不安全
if(bank == null){
bank = new Bank();
}
return bank;
}
}
解决方法一:同步方法
package Program1;
/**
* 使用同步机制,将单例模式中的懒汉式改写为线程安全的
* @author banana
* @create 2023-02-16 15:29
*/
public class BankTest {
}
//懒汉式的单例实现模式
class Bank{
private Bank(){
}
private static Bank bank = null;
//此时的同步监视器就是类本身:Bank.class
public static synchronized Bank getBank(){
//对该if语句的抢占可能最终导致线程的不安全
if(bank == null){
bank = new Bank();
}
return bank;
}
}
解决方法二:同步代码块
package Program1;
/**
* 使用同步机制,将单例模式中的懒汉式改写为线程安全的
* @author banana
* @create 2023-02-16 15:29
*/
public class BankTest {
}
//懒汉式的单例实现模式
class Bank{
private Bank(){
}
private static Bank bank = null;
public static synchronized Bank getBank(){
//这里的同步监视器是类自身的对象
//由于是静态方法,所以里面不能使用this
synchronized (Bank.class) {
if(bank == null){
bank = new Bank();
}
return bank;
}
}
}
方法三、优化
package Program1;
/**
* 使用同步机制,将单例模式中的懒汉式改写为线程安全的
* @author banana
* @create 2023-02-16 15:29
*/
public class BankTest {
}
//懒汉式的单例实现模式
class Bank{
private Bank(){
}
private static Bank bank = null;
public static synchronized Bank getBank(){
//对于后来的线程,直接判断是否创建好了对象,如果创建好了
//就不用等着去到同步块里执行了,而是直接return
//有助于增加运行的效率,减少没有意义的等待
if(bank == null){
synchronized (Bank.class){
return bank = new Bank();
}
}
return bank;
}
}
2、同步机制中的锁:
(1)同步锁机制:在《Thinking in Java》中是这么说:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方式就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而其被解锁之时,另一个任务就可以锁定并使用它了。
(2)synchronized的锁是什么?
任意对象都可以作为同步锁,所有对象都是自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class)、非静态方法(this)
同步代码块:自己指定,很多时候也是可以指定为this或类名.class(即类的对象)
(3)注意:
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要吗否则就无法保证共享资源的安全;
一个线程类的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定序谨慎)。
4.3 线程的死锁问题:
4.3.1 概念:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
对应的解决方法:
专门的算法、原则;
尽量减少同步资源的定义;
尽量避免嵌套同步;
4.3.2 死锁的举例:
package Program1;
/**
*
* 演示线程的死锁:
*
* 1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方
* 放弃自己需要的同步资源,就形成了线程的死锁。
*
* 2.说明:
* 1) 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
* 2) 我们使用同步时,要避免死锁的出现
*
* @author banana
* @create 2023-02-16 16:02
*/
public class Thread_deathlockTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
//继承Thread匿名多线程
new Thread(){
@Override
public void run() {
synchronized(s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
//Runnable匿名实现多线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized(s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
/*
解释:
当一方占用s1监视器,并且想要获取s2监视器的同时,一方占用s2同步监视器时,并且想要获取
s1监视器时发送死锁。
*/
4.4 锁(Lock):
package Program1;
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决线性安全问题的方式三:Lock(锁) ---JDK5.0新增
*
*步骤:
* 1.实例化ReentrantLock
* 2.调用锁定的方法:lock();
* 3.调用解锁的方法:unlock();
*
*
* 面试题:lock和cynchronized
* 相同:二者都可以解决线程安全问题
* 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
* Lock需要手动地去启动同步(lock()方法),同时结束同步也需要手动的实现(unlock())
*
* 优先使用顺序:Lock -> 同步代码块(已经进入方法体,分配了相应的资源) -> 同步方法(在方法体之外)
*
* @author banana
* @create 2023-02-16 16:56
*/
class Window implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2.调用锁定的方法:lock();
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 售票, 票号为:" + ticket);
ticket --;
}
else break;
}finally {
//3.调用解锁的方法:unlock();
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
5、线程的通信
5.1 概念:
线程之间的交互我们就称之为线程通信。
5.2 使用:
package Program2;
/**
*
* 线程通信的例子:使用两个线程打印1-100,线程1,线程2交替打印
* 涉及到三个方法:
* 1.wait():一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器
* 2.notify():一旦执行此方法,就会唤醒被wait()的一个线程,如果有多个线程被wait(),就根据优先级来唤醒
* 3.notifyAll():一旦执行此方法,就会唤醒所有被wait()的线程
*
* 注意点:
* 1.wait()、notify()、notifyAll()使用的时候必须使用在同步代码块或同步方法中。
* 2.wait()、notify()、notifyAll()的调用者必须是同步代码块或同步方法中的同步监视器。
* 否则会出现IllegalMonitorStateException
* 3.这三个方法是定义在java.lang.Object类中的,因为要保证任何一个对象都有该方法,可以让
* 任何对象去充当同步监视器。
*
* @author banana
* @create 2023-02-17 9:53
*/
class Number implements Runnable{
private int number = 1;
@Override
public void run() {
while(true){
synchronized (this) {
//唤醒一个阻塞的线程,加入就绪队列中
//notify():唤醒所有阻塞的线程
notify();
if(number <= 100){
try {
//阻塞当前的调用的线程
//不释放锁
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number ++;
try {
//使得调用如下wait方法的线程进入阻塞状态
//并且释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else break;
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
5.3 sleep方法和wait的异同:
1.相同:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:
1)两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
2)调用的范围不同:sleep可以在任何需要的场景下调用,wait必须使用在同步代码块和同步方法中(通过同步监视器调用)。
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep不会释放锁,wait会释放锁。
6、JDK5.0新增线程创建方式
方式一:实现Callable接口
- 相比run()方法,其call()方法可以有返回值。
- 方法可以抛出异常,对于run()只能用try-catch去解决异常
- 支持泛型的返回值
- 需要借助胡FutureTask类,比如获取返回结果。
package Program3;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 创建线程的方式三:实现Callable接口 ————JDK 5.0新增
*
*
* 具体过程:
* 1.创建一个实现callable的实现类
* 2.实现call方法,将此线程需要执行的操作声明在call()中
* 3.创建Callable接口实现类的对象
* 4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
* 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
* 6.获取Callable中call方法的返回值
*
*
*
* 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程强大?
* 1.call方法可以有返回值的
* 2.call方法可以抛出异常,被外面的操作捕获,获取异常的信息
* 3.Callable是支持泛型的
*
* @author banana
* @create 2023-02-27 21:16
*/
// 1.创建一个实现callable的实现类
class NumThread implements Callable{
// 2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 0; i <= 100; i ++){
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
// 3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
// 4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
/*
FutrueTask是Futrue接口的唯一实现类。
FutrueTask同时实现了Runnable,Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到
Callable的返回值
Futrue接口:可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
*/
FutureTask futureTask = new FutureTask(numThread);
// 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
// 6.获取Callable中call方法的返回值
try {
//get() 返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
方拾二:线程池的方法
问题: 经常创建和销毁、使用特别大的资源,比如并发情况下的线程,对性能影响很大。
解决思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创销毁、实现重复利用。
好处:
-
提高相应速度,减少了创建新线程的时间。
-
降低资源消耗,重复利用线程池中的线程,不需要每次都创建。
-
便于线程管理:
corePoolSize:核心池大小
maximumPoolSize:最大线程数
KeepAliveTime:线程没有任务时最多保持多长时间后会终止。
……
线程池相关API:
/*
JDK 5.0起提供了线程池相关API:ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable.
<T>Future<T>sunmit(Callable<T>task): 执行任务,有返回值,一般又来执行Callable
void shutdown():关闭线程池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor():创建一个只有一个线程池的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
*/
具体代码实现:
package Program3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
*
* 创建线程四:使用线程池
*
* 具体过程:
* 1. 提供指定线程数量的线程池
* 2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
* 3. 关闭线程池
*
*
* @author banana
* @create 2023-02-27 22:28
*/
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0; i <= 100; i ++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0; i <= 100; i ++){
if(i % 2 == 1){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
//ExecutorService 是接口
//返回接口实现类
ExecutorService service = Executors.newFixedThreadPool(10);
//设置线程池的属性(体现管理线程池)
ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
service1.setCorePoolSize(15);
//service1.setKeepAliveTime();
//2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread()); //适合使用于Runnable
service.execute(new NumberThread1()); //适合使用于Runnable
//service.submit(); //适合使用于callable
//3. 关闭线程池
service.shutdown();
}
}
7、其他个人总结
1、对于Thread中的方法currentThread是用来返回当前的线程。比方说,我们在一个Thread继承类中的run重写方法中写到了Thread.currentThread.getName()即可以得到当前执行该run方法线程的名称。那么我们在创建该Thread继承类的对象之后(即实例化之后),如果我们是直接通过其对象去调用run方法,那么当前的线程是main线程。只有当我们用该对象去调用Thread方法中的start方法,才表示开启一个线程,一般默认第一个线程的名称为Thread0。并且,由于该类是Thread继承类,currentThread是Thread的静态方法,因此,我们在类中可以将Thread.currentThread省略不写,直接写getName()来获取当前线程的名称。
2、关于同步方法的总结,同步方法中的代码是对共享资源(多个线程共同操作的变量)的操作代码,同步方法仍然涉及到同步监视器(俗称:锁,任何一个类的对象都可以充当锁,但要求多个线程必须要共用同一把锁,才能实现线程的同步),只是不需要我们显式的声明。对于非静态的同步方法,其同步监视器是this。静态的同步方法,同步监视器是当前类本身。
3、同步和异步:
同步指同一时间,多个线程只能有一个执行(线程并发执行)
异步指同一时间,多个线程同时执行(线程并行执行),相互之间也可以进行通信。
4、
释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该同步代码块,该同步方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作:
-
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
-
线程执行同步代码块时,其他线程调用了该线程的suspend()方法(挂起,由执行到阻塞),该线程不会释放锁(同步监视器)。
应尽量避免使用suspend()和resume()来控制线程。