Java从入门到精通!第十六天,重点!(多线程和线程池)

一、多线程

1.基本概念

(1)程序(Program):

为了完成特定的任务,用某种计算机语言编写的一组指令的集合,即指一段静态的代码(源代码经编译之后形成的二进制格式的文件),静态对象。

(2)进程(Process):

程序的一次执行的过程,或正在执行的程序,是一个动态的过程,有它自身的产生,存在和消亡过程(生命周期)。

进程作为资源分配的单位,系统在运行的时候会为每个进程分配不同的内存区域。

(3)线程(Thread):

进程可以进一步划分为线程,线程是程序内部一条执行路径,如果一个进程同时并行的执行了多个线程,就是支持多线程的。线程作为调用和执行的单位,每个线程拥有独立的运行栈和程序计数器(PC),进程也可以是多进程的程序,但是多进程之间的切换会有很大的开销,线程相对于进程来说切换的开销要小很多。所以,很多时候我们不去设计多进程的程序反而设计成多线程的程序。一个进程中的多个线程可以共享相同的内存单元,它们从同一个堆中分配对象,也可以访问相同的变量和对象,这使得线程之间通讯更简便,高效,但是多个线程共享资源的时候可能会有线程安全问题。

(4)单核 CPU 和多核 CPU 的理解

① 单核 CPU:其实是一个假的多线程,这时候多个线程轮流使用 CPU,我们将这个使用 CPU 的过程称为“时间片”,当轮流使用 CPU 切换的速度极其快的时候,就像是有多个任务在并行执行。

② 如果是多核 CPU 的话,那么可以做到真正的并行,即不同的线程享受不同的 CPU 内核(现在的服务器都是多核的)

③ 并行和并发

a.并行:多个 CPU 同时执行多个任务,例如多个人同时做不同的事情

b.并发:一个 CPU(利用时间片)同时执行多个任务,比如:多个人同时做同一件事情。

(5)使用多线程的优点:

① 提高响应的速度,比如图形界面,可以增强用户的体验度(异步模式)。

② 提高计算机系统的 CPU 的利用率。

③ 改善程序结构,将即长又复杂的进程代码分割成多个线程代码(不能过多),独立运行,利于理解和修改。

(6)什么时候应该使用多线程

① 程序需要同时执行两个或多个任务时

② 程序需要实现一些需要等待的任务时,例如输入,文件读写(比较耗时的操作)

③ 网络操作,搜索操作等

④ 需要在后台运行的程序

(7)创建线程的方式,有两种:

① 通过继承 java.lang.Thread 类来完成

② 通过实现 Runnable 接口来完成                                                                           

步骤:

a. 定义类继承 Thread 类,并重写 run() 方法,其中 run() 方法就是线程的执行体,注意:run() 方法是由虚拟机自动调用的线程执行体方法,不是程序员自己调用的,如果程序员自己去调用 run() 方法,那么线程就失去意义了,run() 是在当前线程获得 CPU “时间片”的时候由 JVM 自动调用。

示例:

从结果可知,其中一个线程打印到30的时候,它的 CPU 的“时间片”结束了,另一个线程获取到了 CPU 时间片然后打印,这里出现了插队的现象。

2)Thread类的构造器

public Thread():创建一个新的Thread对象

public Thread(String name):创建一个线程并指定名称

public Thread(Runnable target):通过传递一个 Runnable 的实现类对象来创建一个线程对象

public Thread(Runnable target, String name):通过一个Runnable接口的实现类对象来创建一个线程对象,并且指定线程名称。

示例:

注意:Thread 的 start() 调用一次之后就不能再调用,否则会抛出 IllegalThreadStateException 异常:

3)通过实现 Runnable 接口创建线程

由于 Java 是单继承,如果继承 Thread 来创建线程,那么这个线程类就不能再继承其他类,但如果是实现Runnable 接口来创建线程,那么线程类还可以继承其他类

步骤:

a. 定义一个实现 Runnable 接口的类,并覆盖 run() 方法,run() 方法就是线程的执行体

b. 将 Runnable 接口的实现类对象传递给 Thread 类的构造器,创建线程对象

c. 通过线程对象的 start() 方法启动线程

示例:

 

void start():启动线程,注意,如果已经调用了start(),再次调用start()方法会抛 IllegalThreadStateException。

public void run():线程被调用时执行的方法

String getName():获取线程的名称

void setName(String name):设置线程的名称

static Thread currentThread():返回当前线程的实例,在 Thread 类中就是指 this。

static void yield():线程让步,暂停当前正在执行的线程,把执行机会让给优先级相同或较高的线程,若线程队列中没有同优先级的线程,则忽略该方法。

join():当某个线程执行流程中调用其他线程的 join 方法时,调用线程(当前线程)将被阻塞,直到 join() 方法加入的其他线程执行完为止。

示例:

package com.edu.th;

public class ThreadDemo3 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
//每个 main 方法就是一个main 主线程,在 main 方法中创建的其他线程就是子线程
Thread thread = Thread.currentThread();//获取当前线程
System.out.println(thread);

SumThread st = new SumThread();
st.start();//启动子线程,完成计算

//如果不加上st.join(),那么主线程main 执行完了可能子线程都还没执行,得到的结果就不正确
try {
st.join();//调用子线程的 join 方法,那么主线程 main 就会在这里阻塞,直到子线程 st 执行完,主线程才能继续往下执行
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println(st.getSum());
}

}

class SumThread extends Thread {
int sum = 0;

@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 1; i <= 100; i++) {
sum += i;
}
}

public int getSum() {
return sum;
}
}

static void sleep(long millis):休眠多少毫秒,让当前活动的线程在指定时间段内放弃使用CPU,使其他线程有机会获得CPU来执行,时间到后重新到线程队列中排队等待执行。

stop():强制结束线程,不推荐使用

boolean isAlive():判断线程是否活跃(run)

(8)多线程的内存图解

多线程执行时,到底在内存中是如何运行的呢?

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

(9)线程调度策略:

① 时间片方式:一个 CPU 让不同的线程轮流享受 CPU ,然后急速的切换。

② 抢占式:高优先级的线程抢占 CPU,有可能低优先级的线程最后才能享受 CPU,高优先级并不意味着这种就一直享受 CPU,只能说高优先级的线程获得 CPU 的几率比低优先级的高。

(10)Java 多线程的调度方法:

① 同优先级的线程组成先进先出队列(FIFO),即先到先服务,使用时间片策略。

② 对于高优先级的线程,使用抢占式策略。

(11)线程的优先级:

① MAX_PRIORITY:10

② MIN_PRIORITY:1

③ NORM_PRIORITY:5

涉及到优先级的方法有:

getPriority():获取线程优先级

setPriority(int priority):设置线程优先级

示例:

线程2获得 CPU 的几率要高一些。

(12)线程的分类

① 主要分为两类:用户线程和守护线程,这两种线程几乎是一致的,唯一的区别是判断 JVM 何时离开。

② 守护线程是用来服务用户线程,通过在 start() 方法之前调用 setDaemon(true) 来将用户线程设置为守护线程。

③ 典型的 Java 垃圾回收器就是一个守护线程。

④ 如果 JVM 中所有的线程都是守护线程,当前 JVM 退出。

所有线程设置为守护线程后,程序运行起来马上就退出。

(13)线程的生命周期

一个完整的线程的生命周期会经历如下 5 个阶段:

1)新建:

当一个 Thread 类或其子类的对象被声明并创建,新生的对象处于线程新建状态。

2)就绪:

处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已经具备了运行的条件,只是没有分配到 CPU 资源

3)运行:

当就绪的线程被调度并获得 CPU 资源的时候,便进入运行状态,此时会执行 run() 方法。

4)阻塞:

当某种特殊情况下,被认为挂起或执行输入输出操作时,让出 CPU 并临时终止自己的运行,进入阻塞状态。

5)死亡:

线程完成了 run() 方法中所有的操作或被强制性的终止或发生异常

2.线程同步

(1)举例:比如家里面有3000块钱,你取了2000,同时你媳妇也取了2000。

(2)线程同步问题:

① 多个线程的执行的不确定性引起执行结果的不稳定。

② 多个线程操作同一个共享数据时,会造成操作的不完整,会破坏数据。

示例:售票系统

package com.edu.th;

public class SellTicketsDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket th1 = new Ticket("用户1");//现在这三个线程对象共享 count 静态成员变量
Ticket th2 = new Ticket("用户2");
Ticket th3 = new Ticket("用户3");
th1.start();
th2.start();
th3.start();
}

}
class Ticket extends Thread {
private static int count = 100;//飞机票的总数,是静态成员,被所有 Ticket 实例所有共享

public Ticket(String name) {
// TODO Auto-generated constructor stub
super(name);
}

@Override
public void run() {
// TODO Auto-generated method stub
while(count > 0) {
try {
Thread.sleep(100);//休眠 100 毫秒,模拟售票消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",购票成功,还剩:" + count--);
}
}
}

发现上面的例子中的数据是混乱的,其原因是因为有三个线程同时操作共享的 count 资源,由于各个线程的调度的不确定性,可能一个线程还没修改完 count 值的时候,其他线程就进来修改,造成数据混乱。

第二种实现方式:

package com.edu.th;

public class SellTicketsDemo2 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket2 tk = new Ticket2();
/**
* 此时虽然 Ticket2 的 count 是实例变量,但是 Ticket2 的实例只有一个,并且三个线程使用的是同一个 Ticket2 实例,
* 所以 Ticket2 的实例变量 count 也是被下面三个线程共享的
*/
Thread th1 = new Thread(tk, "用户1");
Thread th2 = new Thread(tk, "用户2");
Thread th3 = new Thread(tk, "用户3");

th1.start();
th2.start();
th3.start();
}

}
class Ticket2 implements Runnable {
private int count = 100;//飞机票的总数,实例变量

@Override
public void run() {
// TODO Auto-generated method stub
while(count > 0) {
try {
Thread.sleep(100);//休眠 100 毫秒,模拟售票消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",购票成功,还剩:" + count--);
}
}
}

数据也是混乱的。

解决方案:对多线程来操作共享数据的时候,只让一个线程执行,在执行的过程中,不让其他线程进来,而是等待当前线程修改完共享数据之后,才让其他线程进来,这就是我们要讲的加锁的机制。

(3)Java中解决线程安全问题的解决方案:线程同步,有两种方式

1) 同步代码块

2) 同步方法

说明:synchronized 锁是什么

a. 任意对象都可以作为同步锁,所以对象都自动包含有单一的锁

b. 同步方法的锁:静态方法使用“类型.class”加锁,非静态方法使用“对象”加锁

注意:必须确保使用同一个资源的多个线程共用一把锁,否则加锁将没有任何作用

示例:

package com.edu.th;

public class SellTicketsDemo2 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket2 tk = new Ticket2();
/**
* 此时虽然 Ticket2 的 count 是实例变量,但是 Ticket2 的实例只有一个,并且三个线程使用的是同一个 Ticket2 实例,
* 所以 Ticket2 的实例变量 count 也是被下面三个线程共享的
*/
Thread th1 = new Thread(tk, "用户1");
Thread th2 = new Thread(tk, "用户2");
Thread th3 = new Thread(tk, "用户3");

th1.start();
th2.start();
th3.start();
}

}
class Ticket2 implements Runnable {
private int count = 100;//飞机票的总数,实例变量

@Override
public void run() {
/**
* 通过 synchronized(this) 同步代码块加锁之后,如果一个线程进入到该同步代码块操作,此时就对代码块加锁,其他线程
* 试图进入该代码块就会被阻塞在外部等待,直到该线程执行完所有操作走出同步代码块释放锁之后,其他线程才能进入同步代码块
* 进行加锁,以此类推......,这样就没有线程安全问题了,称为线程同步。
*/
//由于三个线程用的都是同一个 tk 对象,而 this 指的就是 tk 对象,所以 this 放在这里作为锁的话,那么三个线程就是共用同一把锁,这样加锁没有问题
synchronized (this) {
// TODO Auto-generated method stub
while (count > 0) {
try {
Thread.sleep(100);//休眠 100 毫秒,模拟售票消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",购票成功,还剩:" + count--);
}
}
}
}

此时数据没有换乱,换句话说,线程同步了。

但是此时只有线程1执行了所有代码,其他线程出现了“饿死”的情况,主要是因为这把锁是不公平锁。

示例2:如果多个线程使用的不是同一把锁,相当于没有加锁,还是会有线程同步的问题

package com.edu.th;

public class SellTicketsDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket th1 = new Ticket("用户1");//现在这三个线程对象共享 count 静态成员变量
Ticket th2 = new Ticket("用户2");
Ticket th3 = new Ticket("用户3");
th1.start();
th2.start();
th3.start();
}

}
class Ticket extends Thread {
private static int count = 100;//飞机票的总数,是静态成员,被所有 Ticket 实例所有共享

public Ticket(String name) {
// TODO Auto-generated constructor stub
super(name);
}

@Override
public void run() {
synchronized (this) {//由于上面产生了三个不同的 Ticket 对象,这里的 this 就表示有三把不同的锁,相当于没有加锁
// TODO Auto-generated method stub
while (count > 0) {
try {
Thread.sleep(100);//休眠 100 毫秒,模拟售票消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",购票成功,还剩:" + count--);
}
}
}
}

数据还是出现了混乱,线程没有同步

要解决上面的问题,必须使用“一把锁”的方案:

package com.edu.th;

public class SellTicketsDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket th1 = new Ticket("用户1");//现在这三个线程对象共享 count 静态成员变量
Ticket th2 = new Ticket("用户2");
Ticket th3 = new Ticket("用户3");
th1.start();
th2.start();
th3.start();
}

}
class Ticket extends Thread {
private static int count = 100;//飞机票的总数,是静态成员,被所有 Ticket 实例所有共享
//创建一个静态的对象 lock 作为锁,静态成员被所有实例共享,那么上面三个线程看到的 lock 锁就是同一把锁了
private static Object lock = new Object();
public Ticket(String name) {
// TODO Auto-generated constructor stub
super(name);
}

@Override
public void run() {
synchronized (lock) {//上面三个线程共用同一把锁 lock
// TODO Auto-generated method stub
while (count > 0) {
try {
Thread.sleep(100);//休眠 100 毫秒,模拟售票消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",购票成功,还剩:" + count--);
}
}
}
}

加锁成功,线程同步了。

3) 同步的范围

a. 如何找到代码中是否存在线程安全问题

a) 明确哪些代码是多线程运行代码

b) 明确多线程是否有共享数据

c) 明确多线程运行代码中是否有多条语句操作共享数据

b. 解决线程安全问题

a) 对于多条操作共享数据的语句,只让一个线程执行,在执行的过程中,其他线程不能参与进来。

b) 在设计同步代码块的时候需要注意:

i. 同步代码块范围太小:没有锁住所有安全问题的代码。

ii. 同步代码块范围太大:没有发挥多线程的优势。

4) 释放锁的时间

a. 当线程执行完同步方法或同步代码块时。

b. 当线程的同步方法或同步代码块遇到了 break,return 等语句时终止了该代码时。

c. 当线程的同步方法或同步代码块遇到了未处理的 Error 或异常时。

d. 当线程的同步方法或同步代码块中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁。

5) 不会释放锁的操作

a. 线程执行到同步方法或同步代码块时,调用 Thread.sleep() 或 Thread.yield() 方法暂停当前线程。

b. 线程执行到同步方法或同步代码块时,程序调用了 suspend 挂起线程也不释放锁。

注意:应该避免使用 suspend() 和 resume() 方法

(4)针对懒汉式单例设计模式存在线程安全问题的解决方法:

(5)线程死锁

1) 死锁:

不同的线程分别占用了对方需要同步的资源不放,都在等待对方放弃自己需要同步的资源,就会形成死锁。

2) 死锁的示例:

package com.edu.th;

public class DeadLockDemo {
private static Object o1 = new Object();//第一把锁
private static Object o2 = new Object();//第二把锁

public static void main(String[] args) {
// TODO Auto-generated method stub
//匿名对象
new Thread(new Runnable() {//匿名内部类

@Override
public void run() {
// TODO Auto-generated method stub
//形成一个同步代码块的嵌套,从而制造死锁
synchronized (o1) {//第一个同步块,使用 o1 加锁
try {
Thread.sleep(200);//休眠200毫秒,模拟线程运行锁消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o2) {//嵌套加上 o2 锁
try {
Thread.sleep(200);//休眠200毫秒,模拟线程运行锁消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("线程1执行完毕");
}
}
}
},"线程1").start();

//匿名对象
new Thread(new Runnable() {//匿名内部类

@Override
public void run() {
// TODO Auto-generated method stub
//形成一个同步代码块的嵌套,从而制造死锁
synchronized (o2) {//第一个同步块,使用 o2 加锁
try {
Thread.sleep(200);//休眠200毫秒,模拟线程运行锁消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o1) {//嵌套加上 o1 锁
try {
Thread.sleep(200);//休眠200毫秒,模拟线程运行锁消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("线程1执行完毕");
}
}
}
},"线程2").start();
}

}

第一个线程获取到 o1 锁之后企图去获取 o2 锁,第二个线程获取到 o2 锁之后企图去获取 o1 锁,此时线程1占着o1 锁,那么线程2 等待,同时线程2占着 o2 锁,那么线程1等待,就会陷入相互等待对方释放锁,从而形成死锁。

3) 死锁的解决方案

a. 有专门的算法,原则

b. 尽量较少同步资源的定义

c. 避免同步块的嵌套

3.Lock 锁(属于J.U.C(java.util.concurrent),适合高并发)

(1)从 JDK5.0 开始,Java 提供了 Lock 对象来作为同步锁。

(2)Lock 对象是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应该先获得锁。

(3)ReentrantLock 类实现了 Lock 接口,它用于 synchronized 相同的并发性和内存语义,在线程安全中,可以显式的加锁和释放锁。

(4)ReentrantLock 类包含两个构造函数,默认构造函数 public ReentrantLock() 会创建不公平锁,带参的构造函数 public ReentrantLock(boolean fair) 传入 true 表示创建公平锁,传入 false 表示创建不公平锁。

示例:

package com.edu.th;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTicketsDemo3 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket3 tk = new Ticket3();
/**
* 此时虽然 Ticket2 的 count 是实例变量,但是 Ticket2 的实例只有一个,并且三个线程使用的是同一个 Ticket2 实例,
* 所以 Ticket2 的实例变量 count 也是被下面三个线程共享的
*/
Thread th1 = new Thread(tk, "用户1");
Thread th2 = new Thread(tk, "用户2");
Thread th3 = new Thread(tk, "用户3");

th1.start();
th2.start();
th3.start();
}

}
class Ticket3 implements Runnable {
private int count = 100;//飞机票的总数,实例变量
private Lock lock = new ReentrantLock(true);//创建一个 ReentrantLock 锁对象,是公平锁
@Override
public void run() {
// TODO Auto-generated method stub
/**
* 加锁之后(lock.lock()),如果一个线程进入到该同步块中操作,那么其他线程会被阻塞在外部等待,直到该线程做完所有操作释放
* 锁(lock.unlock())之后,其他线程才能进来,这样就没有线程安全问题了,称为线程同步。
*/
while (true) {
lock.lock();//加锁
try {
Thread.sleep(100);//休眠 100 毫秒,模拟售票消耗的时间
if(count > 0)
System.out.println(Thread.currentThread().getName() + ",购票成功,还剩:" + count--);
else
break;
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {//Lock 锁的释放一般都是放在 finally 语句块中,表示锁在任何情况下都必须释放
lock.unlock();//释放锁
}

}
}
}

打印的数据没有问题,所以线程是安全的,同时三个线程轮流打印,不存在线程“饿死”的情况,因为我们加的锁是公平锁。

现在我们来看加不公平锁的情况:

package com.edu.th;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTicketsDemo3 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Ticket3 tk = new Ticket3();
/**
* 此时虽然 Ticket2 的 count 是实例变量,但是 Ticket2 的实例只有一个,并且三个线程使用的是同一个 Ticket2 实例,
* 所以 Ticket2 的实例变量 count 也是被下面三个线程共享的
*/
Thread th1 = new Thread(tk, "用户1");
Thread th2 = new Thread(tk, "用户2");
Thread th3 = new Thread(tk, "用户3");

th1.start();
th2.start();
th3.start();
}

}
class Ticket3 implements Runnable {
private int count = 100;//飞机票的总数,实例变量
private Lock lock = new ReentrantLock();//创建一个 ReentrantLock 锁对象,是不公平锁
@Override
public void run() {
// TODO Auto-generated method stub
/**
* 加锁之后(lock.lock()),如果一个线程进入到该同步块中操作,那么其他线程会被阻塞在外部等待,直到该线程做完所有操作释放
* 锁(lock.unlock())之后,其他线程才能进来,这样就没有线程安全问题了,称为线程同步。
*/
while (true) {
lock.lock();//加锁
try {
Thread.sleep(100);//休眠 100 毫秒,模拟售票消耗的时间
if(count > 0)
System.out.println(Thread.currentThread().getName() + ",购票成功,还剩:" + count--);
else
break;
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {//Lock 锁的释放一般都是放在 finally 语句块中,表示锁在任何情况下都必须释放
lock.unlock();//释放锁
}

}
}
}

“用户1”线程获得了大部分的执行机会,”用户2”线程获得了少部分的执行机会,”用户3”线程被“饿死”了,所以不公平锁可能存在线程“饿死”的情况。

(5)对比 synchronized 和 Lock

① Lock 是显式锁,需要手动加锁和释放锁,synchronized 是隐式锁,出了作用域就自动释放锁。

② Lock 只有代码块锁,而 synchronized 有代码块锁和方法锁。

③ 使用 Lock 锁,JVM 将花费比较少的时间来调度线程,性能更好,并且具有很好的扩展性。

4.线程通讯

(1)线程通讯牵涉到的 API:Object 类关于线程通讯的 API

wait():使当前线程挂起并放弃 CPU,同步资源并等待,并且会释放锁,使别的线程可以访问并修改共享资源,当前线程需要排队等待其他线程调用 notify() 或 notifyAll() 唤醒之后再获得锁继续执行。

notify():唤醒正在排队等候同步资源的线程中优先级最高的线程,结束等待,继续执行

notifyAll():唤醒所有正在排队等候同步资源的所有线程,结束等待,继续执行

注意:这三个方法只能在 synchronized 方法或代码块中才能使用,否则会抛出异常

示例:线程通讯,两个线程交替打印 i++

package com.edu.th;

public class ThreadCommunicateDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
CommThread ct = new CommThread();
//两个线程共享同一个 ct 对象
new Thread(ct, "线程1").start();
new Thread(ct, "线程2").start();
}

}
class CommThread implements Runnable{
int i = 0;
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
/**
* 线程1进入同步代码块中唤醒线程2,并将 i+1 输出,然后 wait() 释放锁并等待线程2唤醒它,线程2被
* 线程1唤醒之后就可以获得锁进入同步代码块,然后唤醒线程1,并将 i+1 输出,然后wait()释放锁并陷入阻塞状态等待线程1唤醒,
* 这样就能实现两个线程交替打印i++的操作(是公平的)
*/
synchronized (this) {
notify();//如果当前线程进入同步代码块,就去唤醒其他线程来进入同步代码块
if(i <= 100) {
System.out.println(Thread.currentThread().getName() + "---" + i++);
}else {
break;
}
try {
wait();//当前线程执行 i++ 执行,陷入等待,等待其他线程唤醒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}

上面的例子是“线程1”和“线程2”交替执行,是公平的,如果我们将 notify() 和 wait() 取出,那么就会变成不公平的,参考之前的 synchronized 代码。

示例2:经典的生产者和消费者线程:生产者生产产品由消费者消费,没有产品时,消费者等待(wait()),当消费者消费完产品时,通知生产者生产产品(notify()),当产品数量达到一定数量时,生产者等待(wait()),等待消费者去消费,当生产者把产品生产好之后,通知消费者消费(notify())。

package com.edu.th;

public class ProducerConsumerDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Clert clert = new Clert();
//两个线程共享同一个 clert 对象
new Producer(clert).start();//启动生产者线程
new Consumer(clert).start();//启动消费者线程
}

}

//店员类
class Clert{
private int product = 0;//初始化产品数量为 0

//消费产品,是一个同步方法,由消费者来调用
public synchronized void getProduct() {
if(product == 0) {//没货了,消费者等待
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {//否则表示生产者已经生产了产品,可以消费
System.out.println("消费者消费了一个产品,还剩:" + --product);
try {
Thread.sleep(300);//休眠模拟消费者消费产品所消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//消费者消费了产品之后通知生产者可以继续生产产品了
notify();
}
}

//生产产品,同步方法,由生产者来调用
public synchronized void setProduct() {
if(product != 0) {//产品不为0,表示有货,则生产者等待,等待消费者消费
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {//否则表示没货了,生产者继续生产产品
System.out.println("生产者生产了" + ++product + "个产品");
try {
Thread.sleep(300);//模拟生产者生产产品所消耗的时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//有产品了,通知消费者可以消费了
notify();
}
}
}

//生产者线程
class Producer extends Thread {
private Clert clert;

    public Producer(Clert clert) {
this.clert = clert;
}

@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
clert.setProduct();//生产者不断的生产产品
}
}
}

//消费者线程
class Consumer extends Thread {
private Clert clert;

    public Consumer(Clert clert) {
this.clert = clert;
}

@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
clert.getProduct();//消费者不断地消费产品
}
}
}

5.JDK5.0 新增的创建线程的方式

(1)实现 Callable 接口:相对于实现 Runnable 接口而言,Callable 更强大(属于J.U.C(java.util.concurrent),适合高并发)

① 相对于 run() ,其可以有返回值

② 方法可以抛出异常

③ 执行泛型返回值

④ 需要借助 FutureTask 类,比如获取返回值

示例:使用 Callable 不用去 join() 线程去阻塞就能直接获得值

(2)使用线程池

如果经常创建和销毁线程,将特别消耗资源,如果是在高并发的情况下,对性能的影响是特别大的,我们可以考虑一次性创建多个线程,将这些线程放在线程池中,要使用的时候从线程池取出使用,使用完归还给线程池,这样可以避免频繁的创建和销毁线程,实现重复利用,效率比较高。

1)线程池的优点:

a. 提高了响应的速度(减少了创建线程和销毁线程的时间)

b. 减低资源消耗(每次从线程池中取出,不用重新创建)

c. 便于管理:corePoolSize:核心池大小,maximumPoolSize:线程池最大大小,keepAliveTime:线程保持多少时间后终止...

2)自定义线程池

a. 要实现一个线程池,我们首先要分析其应该具备哪些功能:

a) 创建一个阻塞队列,用于存放要执行的任务

b) 提供 submit 方法,用于添加新的任务

c) 提供构造方法,指定应该创建多少个线程

d) 在构造方法中,创建好这些线程

b. 代码实现:

package com.edu.th;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ThreadPoolDemo {

    public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
//创建线程池对象
MyThreadPoolExecutor executor = new MyThreadPoolExecutor(5);//创建包含5个线程的线程池
for (int i = 0; i < 100; i++) {
int n = i;
executor.submit(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("当前线程:" + Thread.currentThread().getName() + "执行任务" + n);
}
});
}
}

}
class MyThreadPoolExecutor {
//用于存放线程任务的队列,是一个阻塞队列且线程安全
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(100);//最多100个任务

public MyThreadPoolExecutor(int n) {//创建 n 个线程
for (int i = 0; i < n; i++) {
Thread t = new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
try {
Runnable r = queue.take();//从队列中取出一个任务来运行
r.run();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
t.start();
}
}

//添加任务
public void submit(Runnable r) throws InterruptedException {
queue.put(r);//将任务添加到队列
}
}

3)创建线程池的 API(属于J.U.C(java.util.concurrent),适合高并发)

a. ExecutorService 和 Executors

a) ExecutorService:真正的线程池接口,包含如下接口方法:

void execute(Runnable runnable):执行任务,没有返回值,一般用于执行 Runnable 接口实现类的实例

<T> Future <T> submit(Callable<T> task):执行任务,有返回值

b) Executors 工具类:创建各种线程池

Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池

Executors.newFixedThreadPool(int n):创建 n 个固定数量线程的线程池

Executors.newSingleThreadPool():创建单一线程的线程池

Executors.newSechduledThreadPool():创建一个线程池,可以安排在给定延迟时间后执行或周期性的执行

示例:

示例2:利用线程池创建线程,要求延迟 2 秒后执行

示例3:Callable 在线程池中使用

6.并发容器:ConcurrentHashMap

ConcurrentHashMap 是 Java 中一种线程安全的哈希表实现,属于 Java 并发包 J.U.C(java.util.concurrent)的一部分。它被设计用于在多线程环境中高效地进行并发读写操作。以下是对 ConcurrentHashMap 的详细介绍:

(1)基本概念

线程安全:ConcurrentHashMap 在多个线程同时读取和写入数据时,能够保持数据的一致性和完整性。

高效性:与其他同步容器(如 Hashtable 或 Collections.synchronizedMap(map))相比,ConcurrentHashMap 具有更高的并发性能。

(2)工作原理

ConcurrentHashMap 采用了分段锁(Segmented Locking)的策略,将数据分成多个段(或桶),每个段都有独立的锁。这样,在不同段的数据上可以并发地执行读写操作,从而提高性能。

分段锁:ConcurrentHashMap 默认将其结构分为 16 个段,每个段可以独立锁定。这样,多个线程可以同时访问不同的段而不互相阻塞。

非阻塞读取:对于读取操作,ConcurrentHashMap 使用了一种非阻塞的方式,通常不需要加锁,从而提高了并发性能。

(3)主要方法

put(K key, V value):插入或更新指定的键值对。

get(Object key):根据键获取对应的值。

remove(Object key):移除指定的键及其对应的值。

containsKey(Object key):检查是否包含指定的键。

keySet()、values()、entrySet():获取键、值及键值对的集合。

(4)使用示例

package com.edu.th;

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapDemo {

 public static void main(String[] args) {

  // TODO Auto-generated method stub

  ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();

  //插入数据

  map.put("A", 1);

  map.put("B", 2);

  //读取数据

  Integer value = map.get("A");

  System.out.println(value);

  //移除数据

  map.remove("B");

  //检查键

  boolean exists = map.containsKey("B");

  System.out.println("Key B是否存在:" + exists);

 }

(5)优势与局限

优势:

高并发性能:适用于读多写少的场景,能够在多线程环境中提供良好的性能。

不阻塞的读操作:读取操作几乎没有锁的开销。

局限:

不支持 null 键或值:在 ConcurrentHashMap 中,不能插入 null 键或 null 值。

可能导致的遍历性能下降:在高并发的情况下,遍历操作可能会受到影响,因为可能会有其他线程同时对数据进行修改。

(6)总结

ConcurrentHashMap 是处理并发场景中常用的数据结构,适合在多线程应用中使用。理解其工作原理和使用方法能够帮助开发者有效管理共享数据,减少数据竞争和提高程序的性能。

除了 ConcurrentHashMap 之外,在J.U.C 中还有比如 CopyOnWriteArrayList (线程安全的 List,适合高并发),BlockingQueue 的实现类,例如 ArrayBlockingQueue(线程安全的队列,适合高并发)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/90563.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/90563.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

轨道交通为什么要有信号系统?

轨道交通为什么要有信号系统&#xff1f;轨道交通信号系统与公路信号系统有什么不同&#xff1f; 在轨道交通中信号系统是必不可少的&#xff0c;其根本原因在于&#xff1a;在轨道交通中已经没有办法纯靠人力去保证行车安全。 在公路交通中&#xff0c;信号其实是起辅助作用的…

docker 挂载卷

以下是针对您遇到的问题分步解答和解决方案&#xff1a;一、核心结论 ✅ 可以采用目录方式&#xff1a;您的命令中的 -v /root/nginx05-vol/:/usr/share/nginx/html/ 是正确的目录挂载语法。 ❌ 看不到新文件的可能原因主要集中在 权限问题、缓存机制 或 操作顺序错误 上。二、…

uniapp 报错 Not found ... at view.umd.min.js:1的问题

问题描述&#xff1a; uniapp的app中&#xff0c;当页面中使用多个v-if后会出现这个报错解决方案&#xff1a; 1、在v-if的地方加上key属性&#xff08;key属性要保证唯一&#xff09; 2、用v-show替换v-if&#xff08;不建议&#xff0c;可能会影响业务&#xff09;

水电站自动化升级:Modbus TCP与DeviceNet的跨协议协同应用

水电站的自动化系统就像一个精密的“神经中枢”&#xff0c;既要应对水流变化带来的动态负载&#xff0c;又得保证闸门启闭、水轮机调节等关键动作的精准性。我们去年参与的某水电站改造项目里&#xff0c;就遇到了一个典型问题&#xff1a;中控室的施耐德PLC采用Modbus TCP协议…

基于Matlab图像处理的火灾检测系统设计与实现

随着计算机视觉技术的快速发展&#xff0c;基于图像处理的火灾检测系统在安全监控领域的应用得到了广泛关注。本文提出了一种基于图像处理的火灾检测系统&#xff0c;该系统通过对图像进行预处理、颜色空间转换、阈值化处理和形态学分析&#xff0c;自动检测火灾疑似区域。首先…

信息学奥赛一本通 1593:【例 2】牧场的安排 | 洛谷 P1879 [USACO06NOV] Corn Fields G

【题目链接】 ybt 1593&#xff1a;【例 2】牧场的安排 洛谷 P1879 [USACO06NOV] Corn Fields G 【题目考点】 1. 状压动规 【解题思路】 集合状态&#xff1a;n个元素中&#xff0c;选择x个元素构成的集合&#xff0c;可以由一个n位二进制数表示。第i位为1表示选择第i个元…

SpringBoot创建项目的方式

一、Idea Spring initializr创建&#xff08;Spring 官网下载&#xff09; Spring官网只支持SpringBoot3.0以上&#xff0c;JDK17以上 二、idea Spring inst创建&#xff08;阿里云下载&#xff09; 阿里云可以支持JDK8的版本 Spring版本选择2.7.6&#xff0c;选择合适的依赖添…

云原生 —— K8s 容器编排系统

一、 简介Kubernetes&#xff0c;也称为K8s&#xff0c;是一个开源的容器编排系统&#xff0c;用于自动部署、扩展和管理容器化应用程序&#xff0c;帮助开发者更高效地跨集群管理应用。本文总结了 k8s 的基础概念和技术架构。二、基础概念1. 云原生&#xff08;Cloud Native…

SQLite中SQL的解析执行:Lemon与VDBE的作用解析

(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 在 SQLite 的内部实现中&#xff0c;SQL 语句的解析与执行是一个精妙的过程&#xff0c;涉及词法分析、语法分析、中间代码生成与执行等多个环节。其中&#xff0c;Lemon 工具和 VDBE&#xff08;Virtual Database Engine…

C++学习笔记(十:类与对象基础)

往篇内容&#xff1a; C学习笔记&#xff08;一&#xff09; 一、C编译阶段※ 二、入门案例解析 三、命名空间详解 四、C程序结构 C学习笔记&#xff08;二&#xff09; 五、函数基础 六、标识符 七、数据类型 补充&#xff1a;二进制相关的概念 sizeof 运算符简介 补…

图片查重从设计到实现(4)图片向量化存储-Milvus 单机版部署

Milvus 单机版部署 在 Docker 环境下安装、应用和配置 Milvus 向量数据库可以按照以下步骤进行&#xff0c;涵盖从安装到基础应用的完整流程&#xff1a; 1. 部署前准备 服务器&#xff1a;建议测试环境配置 2 核 CPU、8GB 内存&#xff1b;处理 100 万组向量数据&#xff0c;…

前端版本更新检测机制

&#x1f4cc; 一、为什么需要前端版本更新检测机制&#xff1f;在现代 Web 项目中&#xff0c;我们通常会通过 CDN 或缓存策略来加快页面加载速度&#xff0c;但这也带来了一个问题&#xff1a;用户可能访问的是旧版本的页面或资源&#xff0c;而不会自动更新到最新版本。这在…

Python(09)正则表达式

特殊字符 1. 基本元字符 .&#xff1a;匹配除换行符以外的任意单个字符。 *&#xff1a;匹配前面的元素零次或多次。 &#xff1a;匹配前面的元素一次或多次。 ?&#xff1a;匹配前面的元素零次或一次。 2. 定量符 {n}&#xff1a;匹配前面的元素恰好 n 次。 {n,}&#xff1a;…

k8s容器放开锁内存限制

参考&#xff1a;https://access.redhat.com/solutions/1257953 问题 nccl-test容器docker.io/library/nccl-tests:24.12中跑mpirun&#xff0c;buff设置为NCCL_BUFFSIZE503316480 提示out of memory&#xff1a; pod-1:78:91 [0] include/alloc.h:114 NCCL WARN Cuda failure …

基于Zigee的温度数据采集系统

大家好&#xff0c;本文带来的是单片机课设-基于Zigee的温度数据采集系统。 一、设计内容和要求 基于Zigbee的数据采集系统 1.1设计内容 &#xff08;1&#xff09;分析对比Bluetooth、Zigbee、Lora方式组网的基本原理和性能差异&#xff0c;撰写分析报告&#xff1b; &#xf…

ATH12K 驱动框架分析

文章目录 Linux Wireless 驱动框架深入分析 **1. 核心框架层次结构** **1.1 cfg80211 子系统 (`net/wireless/`)** **1.2 mac80211 子系统 (`net/mac80211/`)** **2. ath12k 驱动架构分析** **2.1 核心管理文件** **2.2 数据路径文件** **2.3 平台接口文件** **2.4 功能模块文件…

OSPF路由协议单区域

RIP的不足 以跳数评估的路由并非最优路径 如果RTA选择S0/0传输&#xff0c;传输需时会大大缩短为3sRIP协议限制网络直径不能超过16跳 收敛速度慢 RIP定期路由更新 – 更新计时器&#xff1a;定期路由更新的时间间隔&#xff0c;默认30秒。 – 失效计时器&#xff1a;失效计时器…

Kubernetes部署与管理Scrapy爬虫:企业级分布式爬虫平台构建指南

引言&#xff1a;Kubernetes在爬虫领域的战略价值在大规模数据采集场景中&#xff0c;​​容器化爬虫管理​​已成为企业级解决方案的核心。根据2023年爬虫技术调查报告&#xff1a;采用Kubernetes的爬虫系统平均资源利用率提升​​65%​​故障恢复时间从小时级缩短至​​秒级​…

Web-Machine-N7靶机攻略

一.环境准备&#xff08;VBox&#xff0c;kali虚拟机&#xff0c;靶机&#xff09; 1.1Vbox下载地址: Downloads – Oracle VirtualBox 1.2将N7导入到这个虚拟机中 1.3将kali和Vbox都设置成桥接模式 1.4开启靶机 若鼠标出不来可以使用组合技,CtrlAltDelete强制退出 二.信息…

用毫秒级视频回传打造稳定操控闭环之远程平衡控制系统技术实践

在工业自动化、远程机器人、无人装备等复杂作业场景中&#xff0c;远程实时操控正逐步取代传统“监控指令”模式&#xff0c;成为提升效率与保障安全的关键能力。尤其在高风险、高精度的应用环境中&#xff0c;操作者不仅要“能控”&#xff0c;更要“看得准、反应快”。 真正…