1 基础概念
1.1 程序,进程,线程
-
程序:是为了完成某个特定的任务,而用某种语言编写的一组指令的集合,即指的是一段静态的代码,静态对象。
-
进程:是程序的一次执行过程,或是一个正在运行的程序。是一个动态的过程:有它自身的产生、存在、和消亡的过程——生命周期。
- 程序是静态的,进程是动态的。系统在运行时会为每个进程分配不同的内存空间。
-
线程:进程可进一步细化为线程,是一个程序内部的执行的路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(PC),一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,这就使得线程间的通信更简便、高效。但多个线程操作共享系统资源可能会带来安全隐患。
- java程启动后,main线程,gc线程,异常处理线程
1.2 并行与并发
-
并行:同时执行多个任务或操作。在计算机中,多核处理器可以同时执行多个指令,每个核心可以独立地执行不同的任务。这使得计算速度更快,可以更高效地处理多个任务。
-
并发:在一段时间内同时处理多个任务或操作,虽然同一时刻只能执行一个任务,但通过快速地切换执行上下文,给人一种同时执行的感觉。常见的例子是操作系统通过时间片轮转调度算法,在多个进程之间进行切换,使得它们看起来是同时运行的。
-
并行是真正的同时执行多个任务,而并发是在有限的时间内交替地执行多个任务。
-
在某些情况下,并行和并发可以同时存在,比如在一个多核处理器上运行多个进程,每个进程内部又有多个线程并发执行。但在其他情况下,并行和并发可能是相互独立的,如在单核处理器上的多任务处理。
完成一个任务需要多少个线程
- io密集型 cpu核数*2
- cpu密集型 cpu核数
1.3 线程使用场景
线程是计算机中实现并发的一种方式,可以在同一个进程内同时执行多个任务。以下是一些常见的线程使用场景:
-
多任务处理:线程适用于处理多个相对独立的任务,例如同时下载多个文件、处理多个客户端请求等。通过将不同任务分配给不同的线程,可以提高系统的响应速度和并发处理能力。
-
用户界面响应:在图形用户界面(GUI)应用程序中,使用线程可以确保界面的响应性能。主线程负责处理用户界面的交互和响应,而其他线程负责后台任务,如数据加载、计算等。这样可以避免用户界面的阻塞,提供流畅的用户体验。
-
并发服务器:在服务器应用程序中,使用线程可以同时处理多个客户端请求。每个客户端请求可以在一个独立的线程中进行处理,从而实现并发处理,提高服务器的吞吐量和响应能力。
-
异步编程:线程还可以用于实现异步编程模型。通过在后台启动线程执行耗时的操作,可以防止主线程被阻塞,使得程序能够继续进行其他任务。异步编程常用于网络通信、文件读写等耗时操作。
以下是一些具体的线程使用场景实例:
-
图片处理应用:假设有一个图片处理应用程序,用户可以选择上传多张图片进行处理。在这种情况下,可以使用线程池来管理图片处理任务。每个上传的图片可以在一个独立的线程中进行处理,从而实现并发处理,提高处理速度,并且不会阻塞用户界面。
-
多线程下载器:考虑一个下载器应用程序,用户可以同时下载多个文件。通过使用多线程技术,可以将每个文件的下载任务分配到不同的线程中执行。这样可以实现同时下载多个文件,并加快下载速度。
-
聊天程序:在实时聊天程序中,可以使用线程来处理接收和发送消息的任务。接收消息可以在后台线程中持续监听消息到达,而发送消息也可以在另一个线程中进行。这样可以保持用户界面的响应性,同时处理消息的接收和发送。
-
并发服务器:对于一个并发服务器,可以使用线程池来处理客户端请求。每个客户端连接可以在一个独立的线程中进行处理,从而实现并发处理能力。这样可以确保服务器能够同时处理多个客户端请求,提高系统的性能和响应能力。
2 线程原理
2.1 线程的调度与时间片
由于 CPU 的计算频率非常高,每秒计算数十亿次,于是,可以将 CPU 的时间从毫秒的维度进行分段,每一小段叫做一个 CPU 时间片。不同的操作系统、不同的处理器,线程的 CPU 时间片长度都不同。假定操作系统的线程一个时间片的时间长度为 20 毫秒(比如 Windows XP),在一个 2GHz 的 CPU 上,那么一个时间片可以进行计算的次数是: 20 亿/(1000/20) =4 千万次,也就是说,一个时间片内的计算量是非常巨大的。 目前操作系统中主流的线程调度方式大都是:基于 CPU 时间片方式进行线程调度。线程只有得到 CPU 时间片,才能执行指令,处于执行状态;没有得到时间片的线程,处于就绪状态,等待系统分配下一个 CPU 时间片。由于时间片非常短,在各个线程之间快速地切换,表现出来特征是很多个线程在“同时执行”或者“并发执行”。线程的调度模型,目前主要分为两种调度模型:分时调度模型、抢占式调度模型。
(1)分时调度模型——系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片调度的分配上,所有线程人人平等。
下图就是一个分时调度的简单例子:三个线程,轮流得到 CPU 时间片;一个线程执行时,另外两个线程处于就绪状态
(2)抢占式调度模型——系统按照线程优先级分配 CPU 时间片。优先级高的线程,优先分配 CPU 时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的 CPU 时间片相对多一些。 由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。 Java 的线程管理和调度是委托给了操作系统完成的,与之相对应, Java 的线程调度也是使用抢占式调度模型。
2.2 线程状态
在不同的系统中线程状态的分类不同
操作系统,线程的状态
java的线程状态
得到java的线程状态
public Thread.State getState(); //返回当前线程的执行状态,一个枚举类型值
Thread.State 是一个内部枚举类,定义了 6 个枚举常量,分别代表 Java 线程的 6 种状态,具体如下:
public static enum State {
NEW, //新建
RUNNABLE, //可执行:包含操作系统的就绪、运行两种状态
BLOCKED, //阻塞 -> 操作系统线程中的阻塞
WAITING, //等待 -> 操作系统线程中的阻塞
TIMED_WAITING, //计时等待 -> 操作系统线程中的阻塞
TERMINATED; //终止
}
1. NEW 状态
通过 new Thread(…)已经创建线程,但尚未调用 start()启动线程,该线程处于 NEW(新建)状态。虽然前面介绍了4种方式创建线程,但是其中的其他三种方式,本质上都是通过new Thread( )创建的线程,仅仅是创建了不同的 target 执行目标实例(如 Runnable 实例)。
2. RUNNABLE 状态
Java 把就绪(Ready)和执行(Running)两种状态合并为一种状态:可执行(RUNNABLE)状态(或者可运行状态)。调用了线程的 start()实例方法后,线程就处于就绪状态;此线程获取到 CPU 时间片后,开始执行 run( )方法中的业务代码,线程处于执行状态。
(1)就绪状态就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远是就绪状态;当前线程进入就绪状态的条件,大致包括以下几种:
- 调用线程的 start()方法,此线程进入就绪状态。
- 当前线程的执行时间片用完。
- 线程睡眠(sleep)操作结束。
- 对其他线程合入(join)操作结束。
- 等待用户输入结束。
- 线程争抢到对象锁(Object Monitor)。
- 当前线程调用了 yield 方法出让 CPU 执行权限。
(2)执行状态
线程调度程序从就绪状态的线程中选择一个线程,作为当前线程时线程所处的状态。这也是线程进入执行状态的唯一方式。
3. BLOCKED 状态
处于阻塞(BLOCKED)状态的线程并不会占用 CPU 资源,以下情况会让线程进入阻塞状态:
(1)线程等待获取锁 等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
(2) IO 阻塞 线程发起了一个阻塞式 IO 操作后,如果不具备 IO 操作的条件,线程会进入阻塞状态。 IO 包括磁盘 IO、 网络 IO 等。 IO 阻塞的一个简单例子:线程等待用户输入内容后继续执行。
4. WAITING 状态
处于 WAITING(无限期等待)状态的线程不会被分配 CPU 时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下 3 种方法,会让自己进入无限等待状态:
● Object.wait() 方法,对应的唤醒方式为: Object.notify() / Object.notifyAll()。
● Thread.join() 方法,对应的唤醒方式为:被合入的线程执行完毕。
● LockSupport.park() 方法,对应的唤醒方式为: LockSupport.unpark(Thread)。
5. TIMED_WAITING 状态
处于 TIMED_WAITING(限时等待)状态的线程不会被分配 CPU 时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下 3 个方法会让线程进入限时等待状态:
● Thread.sleep(time) 方法,对应的唤醒方式为: sleep 睡眠时间结束。
● Object.wait(time) 方 法 , 对 应 的 唤 醒 方 式 为 : 调 用 Object.notify() /Object.notifyAll()去主动唤醒,或者限时结束。
● LockSupport.parkNanos(time)/parkUntil(time) 方法,对应的唤醒方式为:线程调用配套的 LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。
进入 BLOCKED 状态、 WAITING 状态、 TIMED_WAITING 状态的线程都会让出 CPU 的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入 Ready 状态,需要重新获取时间片才能接着运行。
6. TERMINATED 状态
线程结束任务之后,将会正常进入 TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。
3 多线程实战
3.1 Thread方式实现多线程
操作步骤:
- 定义子类继承Thread类。
- 类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法。
案例实现:
启动一个线程,在线程中执行1-10000的偶数打印工作
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 10000; i++) {
if (i % 2 == 0) {
//Thread.currentThread().getName():得到线程的名字
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
}
}
}
测试类
public class Test1 {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
myThread1.start();
MyThread myThread2 = new MyThread();
myThread2.start();
System.out.println(Thread.currentThread().getName() + " main 线程 over");
}
}
main方法中可以增加下列代码来阻塞mainxiancheng
JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
System.out.println("main over");
3.2 Runable方式实现多线程
操作步骤:
- 定义子类,实现Runnable接口。
- 类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 调用Thread类的start方法:开启线程, 调用Runnable子类接口的run方法。
案例实现;
public class ThreadDemo {
public static void main(String[] args) {
//方式1:Thread子类,启动
MyThread thread1 = new MyThread();
thread1.start();
//方式2:Runable方式(推荐的方式)
//优势:可以实现多继承,比如继承BaseDao,然后再实现Runnable
MyTask task = new MyTask();
Thread thread2 = new Thread(task);
thread2.start();
//方式3:匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
}
}).start();
}
}
//方式1
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
}
}
//方式2
//优势:可以实现多继承,比如继承BaseDao,然后再实现Runnable
class MyTask implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
}
}
3.3 Thread常见方法
- 构造函数:
- Thread(): 创建新的Thread对象
- Thread(String threadname): 创建线程并指定线程实例名
- Thread(Runnable target): 指定创建线程的目标对象,它实现了Runnable接口中的run方法
- Thread(Runnable target, String name): 创建新的Thread对象
- void start(): 启动线程,并执行对象的run()方法
- run(): 线程在被调度时执行的操作
- String getName(): 返回线程的名称
- **void setName(String name)😗*设置该线程名称
- static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
- static void yield(): 线程让步
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
- join() : 当某个程序执行流中调用其他线程的 join() 方法时, 调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
- static void sleep(long millis): (指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
- stop(): 强制线程生命期结束,不推荐使用
- boolean isAlive(): 返回boolean,判断线程是否还活着
- interrupt(): 发送中断信号(true)
- 如果线程在阻塞状态,比如sleep(),join(),wait(),这时接收到中断信号会抛出一个异常InterruptException,同时中断信号清除(false)
- 只是发送信号,不会对线程产生影响
- static interrupted(): 得到中断信号(true),然后把中断信号设置成false
- isInterrupted(): 得到中断信号,不会清除中断信号
案例演示:
Thread.sleep()
指定线程休眠的时间,单位毫秒,让出cpu时间片,其他线程可以抢占cpu时间片
public class MyTask implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
//Thread.currentThread().getName():得到线程的名字
System.out.println(Thread.currentThread().getName() + "\t" + i);
try {
Thread.sleep(1000);
Thread.yield();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
Thread.join()
当某个程序执行流中调用其他线程的 join() 方法时, 调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
原理:
有两个线程1,2如下:
执行join()方法后:
演示案例:
public class Test1 {
/**
* CountDownLatch:可以实现相同的效果
* @param args
*/
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " main orver");
}
}
结果:
Thread.stop()
强制线程生命期结束,不推荐使用,线程退出方式粗暴,不管线程正在执行的任务,直接退出,可能丢失数据
演示案例:
public class Test1 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyTask());
t1.start();
Scanner in = new Scanner(System.in);
System.out.println("输入1/0:0表示退出");
int i = in.nextInt(); ///主线程进入IO阻塞
if (i == 0) {
t1.stop();
}
System.out.println("main over");
}
static class MyTask implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
Thread.interrupt()
发送中断信号(true),只是发送信号,不会对线程产生影响
演示案例:
public class Test1 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyTask());
t1.start();
JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
t1.interrupt(); //发送中断信号给t1
System.out.println("main over");
}
static class MyTask implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("a");
try {
Thread.sleep(500000);
} catch (InterruptedException e) {
System.out.println("bbbbbbbbbbbbbbb");
e.printStackTrace();
Thread.currentThread().interrupt(); //再次发送中断信号,中断信号发给阻塞线程,抛出Interrupt异常,中断信号清除
// throw new RuntimeException(e);
}
//得到中断信号,优雅的退出
if (Thread.interrupted()) {
break;
}
}
}
}
}
3.4 线程优先级
- MAX_PRIORITY:10 线程优先级最大值为10
- MIN _PRIORITY:1 线程优先级最小为1
- NORM_PRIORITY:5 默认线程优先级为5
public static void main(String[] args) {
MyTask myTask = new MyTask();
Thread thread1 = new Thread(myTask, "t1");
thread1.setPriority(Thread.MIN_PRIORITY);//设置线程优先级为1
thread1.start();
Thread thread2 = new Thread(myTask, "t2");
thread1.setPriority(Thread.MAX_PRIORITY);//设置线程优先级为10
thread2.start();
}
3.5 守护线程
- 其他线程都执行结束,守护线程自动结束
- 守护启动子线程,也是守护线程
- 守护线程的语法thread.(setDaemon(true)设置守护线程
public class Test2 {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
// myThread1.setDaemon(true);
myThread1.start(); //守护线程, gc线程,jvm线程结束gc会自动结束
JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
System.out.println("main over");
}
}
class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("a");
}
}
}
3.6 多线程自增i++和线程执行原理
Plus plus = new Plus();
PlusTask plusTask = new PlusTask(plus);
Thread t1 = new Thread(plusTask);
Thread t2 = new Thread(plusTask);
Thread t3 = new Thread(plusTask);
Thread t4 = new Thread(plusTask);
如上述代码,创建t1、t2、t3、t4 4个线程对象时同时引用相同线程操作对象plusTask,从而使得t1、t2、t3、t4 4个线程能够操作堆内存中线程所共享的Plus对象。
下面是具体代码实现:
自增类
public class Plus {
private int amount = 0;
public void selfPlus() {
amount ++;
}
public int getAmount() {
return amount;
}
}
自增任务类
public class PlusTask implements Runnable {
private Plus plus;
public PlusTask() {
}
public PlusTask(Plus plus) {
this.plus = plus;
}
@Override
public void run() {
for (int i = 0; i < 100000000; i++) {
plus.selfPlus();
}
}
}
测试
public static void main(String[] args) throws InterruptedException {
Plus plus = new Plus();
PlusTask plusTask = new PlusTask(plus);
Thread t1 = new Thread(plusTask);
Thread t2 = new Thread(plusTask);
Thread t3 = new Thread(plusTask);
Thread t4 = new Thread(plusTask);
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println("实际的值 = " + plus.getAmount());
}
在上述代码中,4个线程自增一个堆(共享的)里的对象的值,即线程t1、t2、t3、t4同时操作amount进行自增操作,又因为自增操作在CPU中不属于原子性操作,从而导致了输出结果远小于理论结果。
自增反编译图:
4 线程同步
4.1 多窗口买票
4.1.1 实现思路
4.1.2 代码实现
Ticket类:
package l_ticket;
public class Ticket {
private int count = 100;
/**
* 查票
* @return
*/
public int getCount() {
return count;
}
public int out() {
return this.count--;
}
}
WindowTask类;
package l_ticket;
public class WindowTask implements Runnable {
private Ticket ticket;
public WindowTask() {
}
public WindowTask(Ticket ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while (true) {
//票数小于0,窗口退出
if (ticket.getCount() <= 0) {
break;
}
//阻塞,会有超卖
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "\t" + ticket.out());
}
}
}
测试类:
package l_ticket;
public class Test1 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
WindowTask task = new WindowTask(ticket);
Thread window1 = new Thread(task, "1窗口");
Thread window2 = new Thread(task, "2窗口");
Thread window3 = new Thread(task, "3窗口");
Thread window4 = new Thread(task, "4窗口");
window1.start();
window2.start();
window3.start();
window4.start();
}
}
4.1.3 测试结果
1窗口 3
2窗口 2
3窗口 0
3窗口 1
4窗口 -1
出现卖出0、-1票的情况,即超卖问题
4.1.4 结果分析
理想状态如下:
极端状态:
- 超卖问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
- 解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行,即完成对临界区代码块的加锁。
- 临界区代码块:多个线程同时访问的代码块
- 共享数据:临界区代码块中,多个线程共享访问的堆里面的数据
- 加锁:通过共享对象给临界区代码块加锁
4.2 synchronized内置锁
4.2.1 概念
1. 临界区资源(共同数据)
表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程必须等待。
2. 临界区代码段(Critical Section)
是每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问。线程进入临界区代码段之前,必须在进入区申请资源,申请成功之后进行临界区代码段,执行完成之后释放资源。临界区代码段(Critical Section)的进入和退出、
在 Hotspot 虚拟机中, Monitor 是由 C++类ObjectMonitor 实现, ObjectMonitor 类定义ObjectMonitor.hpp 文件中,其构造器代码大致如下:
/Monitor 结构体
ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
//线程的重入次数
_recursions = 0;
_object = NULL;
//标识拥有该 monitor 的线程
_owner = NULL;
//等待线程组成的双向循环链表
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
//多线程竞争锁进入时的单向链表
cxq = NULL ;
FreeNext = NULL ;
//_owner 从该双向循环链表中唤醒线程节点
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
4.2.2 synchronized语法
1. 代码块
synchronized (共享对象(plus,ticket)) {//临界区代码块
//对共对象的访问(plus)(ticket)
}
2. 示例
synchronized(对象名):对共享对象加锁
多窗口售票
synchronized (ticket) {
//票数小于0,窗口退出
if (ticket.getCount() <= 0) {
break;
}
//阻塞,会有超卖
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "\t" + ticket.out());
}
}
自增1亿次
synchronized(this):对线程对象加锁
public class SynDemo1 {
public static void main(String[] args) {
Car a = new Car();
Thread t1 = new Thread1(a);
Thread t2 = new Thread2(a);
t1.start();
t2.start();
}
}
class Thread1 extends Thread {
private Car a;
public Thread1(Car a) {
this.a = a;
}
public void run() {
a.fun1();
}
}
class Thread2 extends Thread {
private Car a;
public Thread2(Car a) {
this.a = a;
}
public void run() {
a.fun2();
}
}
class Car {
public synchronized void fun1() {
//synchronized (this) {
System.out.println("开始打蜡");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打蜡结束");
//}
}
public void fun2() {
synchronized (this) {
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始抛光");
System.out.println("抛光结束");
}
}
}
syncronized(class):对静态方法加锁
public class SynDemo1 {
public static void main(String[] args) {
Car a = new Car();
Thread t1 = new Thread1(a);
Thread t2 = new Thread2(a);
t1.start();
t2.start();
}
}
class Thread1 extends Thread {
private Car a;
public Thread1(Car a) {
this.a = a;
}
public void run() {
a.fun1();
}
}
class Thread2 extends Thread {
private Car a;
public Thread2(Car a) {
this.a = a;
}
public void run() {
a.fun2();
}
}
class Car {
public synchronized static void fun1() {
System.out.println("开始打蜡");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打蜡结束");
}
public static void fun2() {
synchronized (Car.class) {
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始抛光");
System.out.println("抛光结束");
}
}
}
4.2.3 使用注意事项
在使用syncronized时必须有相同的锁,在多个线程使用不同的锁时,syncronized不会起作用。
案例演示:
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (String.class) {
System.out.println("a");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("b");
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (Integer.class) {
System.out.println("1");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("2");
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
测试类
public class Test1 {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.start();
t2.start();
}
}
4.3 死锁
synchronized嵌套时,比如张三在A电话亭想去B电话亭,李四在B电话亭想去A电话亭,这时会发生死锁。
a线程锁定一个资源,同时想获取b线程的资源,b线程锁定一个资源,同时想获取a线程的资源。
案例演示:
public class DeadLock {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (String.class) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (Integer.class) {
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Integer.class) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (String.class) {
}
}
}
});
t1.start();
t2.start();
}
}
4.4 CountDownLatch
juc的公共锁
- 构造函数new CountDownLatch(perms 10)授权数量
- countDown():授权数量-1
- await():执行后线程进入阻塞,知道授权数量=0
案例演示:
Plus类
public class Plus {
private int count = 0;
public void selfPlus() {
count ++;
}
public int getCount() {
return count;
}
}
MyTask类
public class MyTask implements Runnable {
Plus plus;
CountDownLatch countDownLatch;
public MyTask() {
}
public MyTask(Plus plus, CountDownLatch countDownLatch) {
this.plus = plus;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
synchronized (plus) {
for (int i = 0; i < 100000000; i++) {
plus.selfPlus();
}
countDownLatch.countDown(); //-1
}
}
}
测试类
public class Test1 {
public static void main(String[] args) {
Plus plus = new Plus();
CountDownLatch countDownLatch = new CountDownLatch(4);
MyTask task = new MyTask(plus, countDownLatch);
for (int i = 0; i < 4; i++) {
new Thread(task).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("理论值 = 400000000");
System.out.println("实际值 = " + plus.getCount());
System.out.println("理论值和实际值相差 = " + (400000000 - plus.getCount()));
}
}
5 线程通讯
5.1 等待唤醒机制
线程的等待机制:
synchronized(内置锁)
- EntryList:双向队列:等待锁的多列
- Owner:正在执行的线程
- WaitSet:双向队列
- locko.wait:wait方法一定在synchronize代码块中,把当前线程放到waitset中
- locko.notify:把waitset队列中的一个随机线程放到entrylist中
- locko.notifyAll:把waitset队列中的所有线程都放到entrylist中
- 所有对象都会关联一个c++结构,这个结构ObjectMonier
/Monitor 结构体
ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
//线程的重入次数
_recursions = 0;
_object = NULL;
//标识拥有该 monitor 的线程
_owner = NULL;
//等待线程组成的双向循环链表
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
//多线程竞争锁进入时的单向链表
cxq = NULL ;
FreeNext = NULL ;
//_owner 从该双向循环链表中唤醒线程节点
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
5.2 生产者消费者模式
5.2.1 实现思路
- 包子铺线程负责生产包子
- 吃货线程负责消费包子
- 包子对象
5.2.2 代码实现
1. 共享对象(Baozi)
public class Baozi {
private String pier;
private String xianer;
private boolean flag = false ;//包子资源 是否存在 包子资源状态
public String getPier() {
return pier;
}
public void setPier(String pier) {
this.pier = pier;
}
public String getXianer() {
return xianer;
}
public void setXianer(String xianer) {
this.xianer = xianer;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public String toString() {
return "Baozi{" +
"pier='" + pier + '\'' +
", xianer='" + xianer + '\'' +
", flag=" + flag +
'}';
}
}
2. 生产者(Baozip)
public class Baozipu extends Thread {
private Baozi baozi;
public Baozipu() {
}
public Baozipu(Baozi baozi) {
this.baozi = baozi;
}
@Override
public void run() {
int count = 0;
while (true) {
synchronized (baozi) {
if (baozi.isFlag()) {
try {
baozi.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (count % 2 == 0) {
baozi.setPier("米皮");
baozi.setXianer("韭菜鸡蛋");
} else {
baozi.setPier("面皮");
baozi.setXianer("猪肉大葱");
}
baozi.setFlag(true); //包子已经生成好了的标记
System.out.println("包子已经生产完毕," + baozi);
System.out.println("吃货来吃包子吧");
count++;
baozi.notify();
}
}
}
}
3. 消费者(ChiHuo)
public class ChiHuo extends Thread {
private Baozi baozi;
public ChiHuo() {
}
public ChiHuo(Baozi baozi) {
this.baozi = baozi;
}
@Override
public void run() {
while (true) {
synchronized (baozi) {
if (!baozi.isFlag()) {
try {
baozi.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("吃货正在吃 " + baozi);
baozi.setFlag(false);
baozi.notify();
}
}
}
}
4. 测试类
public class Test1 {
public static void main(String[] args) {
Baozi baozi = new Baozi();
Baozipu baozipu = new Baozipu(baozi); //包子铺线程
ChiHuo chiHuo = new ChiHuo(baozi); //吃货线程
chiHuo.start();
baozipu.start();
}
}
6 juc显示锁
- 内置锁:synchronized
- 现实锁:juc锁
JDK5 版本引入了 java.util.concurrent 并发包,简称为 JUC 包,JUC 出自并发大师 Doug Lea 之手, Doug Lea 对 Java 并发性能的提升做出了巨大的贡献。
6.1 Lock接口
Java 对象锁还存在性能问题。在竞争稍微激烈的情况下, Java 对象锁会膨胀为重量级锁(基于操作系统的 Mutex Lock 实现),而重量级锁的线程阻塞和唤醒操作,需要进程在内核态和用户态之间来回切换,导致其性能非常低。JUC显示锁因为是纯粹Java语言实现,避免了这些问题,同时JUC显示锁具备了对象锁不具备的高级特性r如限时抢锁、可中断抢锁、多个等待队列。
显式锁不再作为 Java 内置特性来实现,而是作为 Java 语言可编程特性来实现
java.util.concurrent.locks.Lock接口的主要抽象方法如下:
方 法 | 描 述 |
---|---|
void lock() | 抢锁。 成功则向下运行,失败则阻塞抢锁线程 |
void lockInterruptibly() throws InterruptedException | 可中断抢锁,当前线程在抢锁的过程中可以响应中断信号 |
boolean tryLock() | 尝试抢锁, 线程为非阻塞模式,在调用 tryLock 方法后立即返回。抢锁成功返回 true, 抢锁失败返回 false |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 限时抢锁,到达超时时间返回 false。并且此限时抢锁方法也可以响应中断信号 |
void unlock(); | 释放锁 |
Condition newCondition(); | 获取与显式锁绑定的 Condition 对象,用于“等待-通知”方式的线程间通信 |
从 Lock 提供的接口方法可以看出, 显式锁至少比 Java 内置锁多了以下优势:
1. 可中断获取锁
使用 synchronized 关键字获取锁的时候,如果线程没有获取到被阻塞,阻塞期间该线程是不响应中断信号(interrupt)的;而使用 Lock.lockInterruptibly( )方法获取锁时,如果线程被中断,线程将抛出中断异常。
2. 可非阻塞获取锁
使用 synchronized 关键字获取锁时,如果没有成功获取,线程只有被阻塞;而使用Lock.tryLock( )方法获取锁时,如果没有获取成功,线程也不会被阻塞,而是直接返回 false。
3. 可限时抢锁
使用 Lock.tryLock(long time, TimeUnit unit)方法, 显式锁可以设置限定抢占锁的超时时间。而在使用 synchronized 关键字获取锁时,如果不能抢到锁,线程只能无限制阻塞。
6.2 ReentrantLock的基本用法
ReentrantLock 是 Java 中的一个可重入锁实现,它可以用于多线程环境下对共享资源的互斥访问。 与内置的synchronized关键字相比,ReentrantLock 提供了更灵活和强大的锁定机制。
-
可重入性:ReentrantLock 是可重入的,也就是说同一个线程可以多次获得该锁而不会导致死锁。这个特性可以避免在递归函数或相互调用的方法中出现死锁情况。
-
公平性:ReentrantLock 可以选择是否公平地分配锁。在公平模式下,等待时间最长的线程将获得锁的访问权。在非公平模式下,默认是竞争方式获取锁。
-
锁定和解锁:要获取锁,可以使用lock()方法,如果锁已经被其他线程持有,则当前线程会阻塞直到获得该锁。为了释放锁,应该使用unlock()方法。
-
条件变量:ReentrantLock 提供了条件变量(Condition)的支持,通过条件变量可以实现更复杂的线程通信和同步机制。
案例演示:
public class ReenterLockTest implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
public void run() {
lock.lock();
lock.lock();
try {
for (int j = 0; j < 10000000; j++) {
i++;
}
} finally {
lock.unlock();
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockTest r1 = new ReenterLockTest();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
6.3 读写锁
读写锁(Read-Write Lock)是一种并发控制机制,用于提高对共享资源的访问效率。它允许多个线程同时读取共享资源,但在进行写操作时需要独占访问,读写锁适用于读多写少的并发情况。
-
多个线程可以同时获取读锁:当没有线程持有写锁时,多个线程可以同时获得读锁,并发读取共享资源。
-
线程获得写锁时独占访问:如果有线程已经持有读锁或写锁,其他线程请求写锁时将被阻塞,直到当前线程释放写锁。
-
读锁不排斥其他读锁:读锁之间不互斥,多个线程可以同时持有读锁并进行读取操作。
案例实现:
public class ReadWriteLockDemo {
static CountDownLatch countDownLatch;
private static Lock lock = new ReentrantLock();
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private int value;
public Object handleRead(Lock lock) throws InterruptedException {
lock.lock(); //阻塞
try {
Thread.sleep(1000);
System.out.println("read success");
countDownLatch.countDown();
return value;
} finally {
lock.unlock();
}
}
public void handleWrite(Lock lock, int index) throws InterruptedException {
lock.lock(); //阻塞
try {
Thread.sleep(1000);
value = index;
System.out.println("write success");
countDownLatch.countDown();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
final ReadWriteLockDemo demo = new ReadWriteLockDemo();
Runnable readRunnable = new Runnable() {
public void run() {
try {
demo.handleRead(readLock);
// demo.handleRead(lock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable writeRunnable = new Runnable() {
public void run() {
try {
demo.handleWrite(writeLock, new Random().nextInt());
// demo.handleWrite(lock, new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
countDownLatch = new CountDownLatch(20);
long t1 = System.currentTimeMillis(); //1970-1-1 0:0:0:0到现在的毫秒
for (int i = 0; i < 18; i++) {
new Thread(readRunnable).start();
}
for (int i = 18; i < 20; i++) {
new Thread(writeRunnable).start();
}
countDownLatch.await();
long t2 = System.currentTimeMillis(); //1970-1-1 0:0:0:0到现在的毫秒
System.out.println(t2 - t1); //期待20s
}
}
6.4 Semaphore许可管理器
Semaphore 是一个是许可管理器,可以用来控制在同一时刻访问共享资源的线程数量,Semaphore 维护了一组虚拟许可,其数量可以通过构造函数的参数指定。线程在访问共享资源前,必须使用 Semaphore 的 acquire 方法获得许可,如果许可数量为 0,该线程则一直阻塞。线程访问完成资源后,必须使用 Semaphore 的 release 方法去释放许可。
6.4.1 Semaphore 的主要方法
- Semaphore(permits):构造一个 Semaphore 实例,初始化其管理的许可数量为 permits 参数值。
- Semaphore(permits,fair):是否以公平模式(fair 参数是否为 true)进行许可的发放。
- availablePermits( ):获取 Semaphore 对象可用的许可数量。
- acquire( ):尝试获取 1 个许可。而当前线程被中断,则会抛出 InterruptedException 异常并终止阻塞
- acquire(permits):尝试去阻塞的获取 permits 个许可
- acquierUninterruptibly( ):阻塞的过程不可中断,直到成功获取 1 许可。
- acquireUninterruptibly(permits):获取 permits 个许可,阻塞的过程不可中断
- tryAcquire():非阻塞获取1个许可
- tryAcquire(permits):非阻塞获取 permits 个许可。
- tryAcquire(timeout,TimeUnit):限时获取许可
- release( ):释放 1 个可用的许可
- release(permits):释放 permits 个可用的许可。
- drainPermits( ):当前线程获得剩余的所有可用许可。
- hasQueuedThreads( ):判断当前 Semaphore 对象上是否存在正在等待许可的线程。
- getQueueLength( ):获取当前 Semaphore 对象上正在等待许可的线程数量。
6.4.2 Semaphore 案例演示
假设有 10 个人在银行办理业务,只有 2 个工作窗口,使用 Semaphore 模拟银行排队,代码如下:
public class SemaphoreTest {
public static void main(String[] args) throws InterruptedException {
//线程池,用于多线程模拟测试
final CountDownLatch countDownLatch = new CountDownLatch(10);
//创建信号量,含有2个许可
final Semaphore semaphore = new Semaphore(2);
AtomicInteger index = new AtomicInteger(0);
//创建Runnable可执行实例
Runnable r = () ->
{
try
{
//抢占一个许可
semaphore.acquire(1);
//模拟业务操作: 处理排队业务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + ", 受理处理中...,服务号: " + index.incrementAndGet());
Thread.sleep(1000);
//释放一个信号
semaphore.release(1);
} catch (Exception e)
{
e.printStackTrace();
}
countDownLatch.countDown();
};
//创建4条线程
Thread[] tArray = new Thread[10];
for (int i = 0; i < 10; i++)
{
tArray[i] = new Thread(r, "线程" + i);
}
//启动4条线程
for (int i = 0; i < 10; i++)
{
tArray[i].start();
}
countDownLatch.await();
}
}