一、多线程
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(线程安全的队列,适合高并发)。