文章目录
1. 基本介绍
1.1 进程
程序:程序是包含指令和数据的文件,存储在硬盘或其他存储设备中。
进程:进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。每个进程都有自己的内存空间和系统资源。
在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线
程就是这个进程中的⼀个线程,称为主线程。
在平时生活中使用播放器播放视频,此时运行的播放器就是进程。
1.2 线程
因为进程调度、分派、切换时需要花费较大的时间和空间开销,为了提高系统的执行效率,线程便用于取代进程,线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。线程是资源调度的基本单位。
⼀个进程在其执⾏的过程中可以产⽣多个线程。系统产生一个线程或者各个线程之间作切换工作,负担要比进程小得多,所以线程也被称为轻量级进程。
在Java中调用普通方法和启动新的线程调用方法对比图例:
1.3 使用场景
多线程在我们平时开发使用到的框架、工具、服务器中普遍使用到,如Tomcat服务器中处理一个请求就会从其线程池中用一个线程进行处理。
随着计算机的发展,如今CPU很多都是多核的了,如果我们不去用多线程,就只用到一个核心,就造成其他CPU核心的闲置。所以在我们平时开发系统的时候应该多加思考哪里可以使用到多线程进行处理,以提高系统的执行效率。使用场景如下:
- 新建线程执行定时任务,因为定时任务一般执行时间都非常长,启动新线程处理能够提高系统的吞吐量。
- 使用生产者和消费者模式,用多个线程消费消息队列的消息。
- 异步处理
下面介绍线程如何创建。
2. 线程的创建
在Java中,创建线程的三种基本方式:1、继承Thread类 2、实现Runnable接口 3、 实现Callable接口。下面具体介绍这三种方式:
2.1 继承Thread类
使用步骤:
- 自定义线程类继承Thread类
- 重写run方法,编写线程执行体
- 创建线程对象,调用start方法启动线程
代码示例:
public class ThreadTest01 extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("我在写代码"+i);
}
}
public static void main(String[] args) {
ThreadTest01 threadTest01 = new ThreadTest01();
threadTest01.start();
for (int i = 0; i < 1000; i++) {
System.out.println("我在学习多线程"+i);
}
}
}
控制台输出:
线程:main启动
线程:Thread-0启动
我在学习多线程0
我在写代码0
我在学习多线程1
我在写代码1
我在写代码2
我在写代码3
我在写代码4
从结果上看,主线程main和新建的线程Thread-0同时执行,但是从计算机来看,线程启动后,不一定立即执行,需要等待CPU调度。
2.2 实现Runnable接口
因为Java单继承的局限性,并且继承Thread类的方式,会使得继承的对象拥有Thread类的变量和方法,变成“重量级”的类,造成内存空间浪费。所以推荐使用Runnable接口方式创建线程。
实现Runnable接口的方式使用到了静态代理模式,线程执行体由自身完成,线程其他的操作由Thread类代理完成。
使用步骤:
- 实现Runnable接口
- 实现run方法,编写线程执行体
- 创建线程对象,调用start方法启动线程
代码示例:
public class ThreadTest02 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("我在写代码"+i);
}
}
public static void main(String[] args) {
System.out.println("线程:"+Thread.currentThread().getName()+"启动");
//创建实现类
ThreadTest02 threadTest02 = new ThreadTest02();
//创建代理类
Thread thread = new Thread(threadTest02);
thread.start();
System.out.println("线程:"+thread.getName()+"启动");
for (int i = 0; i < 1000; i++) {
System.out.println("我在学习多线程"+i);
}
}
}
2.3 实现Callable接口
Callable接口在JAVA 5时开始引入的,Callable接口可以返回结果或抛出检查异常,但是Runnable不可以。
使用步骤:
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建Runnable的实现类FutureTask,传入Callable接口的实现对象
代码示例:
public class ThreadTest03 implements Callable {
@Override
public Boolean call() throws Exception {
for (int i = 0; i < 20; i++) {
System.out.println("我在写代码"+i);
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadTest03 threadTest03 = new ThreadTest03();
FutureTask futureTask = new FutureTask(threadTest03);
new Thread(futureTask).start();
}
}
3. 线程的状态
Java线程在运行的生命周期中的指定时刻只可能处于以下状态:初始、运行、阻塞、等待、超时等待、终止。下面是这六大状态的说明:
- NEW
初始状态,线程被构建,尚未启动的线程处于此状态 - RUNNABLE
运行状态,Java线程将操作系统中的“就绪”和“运行”两种状态都称为“运行中” 。 - BLOCKED
阻塞状态,被阻塞等待监视器锁的线程处于此状态 - WAITING
等待状态,正在等待另一个线程执行特定动作处于此状态 - TIMED_WAITING
超时等待状态, 正在等待另一个线程执行动作达到执行的等待时间的线程处于此状态 - TERMINATED
终止状态, 已执行结束、退出的线程处于此状态
这六种状态在Thread类定义的内部枚举类State中定义:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
线程状态的轮转图:
Java线程随代码的执行在不同状态切换的状态变更图:
说明:
线程创建之后它将处于 NEW(新建) 状态,调⽤ start() ⽅法后开始运⾏,线程这时候处于 READY(可运⾏) 状态。可运⾏状态的线程获得了 CPU 时间⽚(timeslice后就处于 RUNNING(运⾏) 状态。
当线程执⾏ wait() ⽅法之后,线程进⼊ WAITING(等待) 状态。进⼊等待状态的线程需要依靠其他线程的通知才能够返回到运⾏状态,⽽ TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,⽐如通过 sleep(long millis) ⽅法或 wait(long millis)⽅法可以将Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调⽤同步⽅法时,在没有获取到锁的情况下,线程将会进⼊到 BLOCKED(阻塞)状态。线程在执⾏ Runnable 的 run() ⽅法之后将会进⼊到 TERMINATED(终⽌) 状态。
线程状态转换的常用方法:
3.1 线程停止(STOP)
停止线程不推荐使用JDK提供的stop(),destroy()方法,建议使用一个标志位flag作为终止变量,当flag=false时,就终止线程的执行。
步骤:
- 线程中定义线程体使用的标识
- 线程体使用标识
- 对外提供方法改变标识
代码示例:
public class ThreadStopDemo implements Runnable {
/**
* 定义线程使用的标志
*/
private boolean flag=true;
@Override
public void run() {
while (flag){
System.out.println(Thread.currentThread().getName()+": I am running");
}
}
/**
* 对外提供方法改变标识
*/
public void stop(){
flag=false;
}
public static void main(String[] args) throws InterruptedException {
ThreadStopDemo threadStopDemo = new ThreadStopDemo();
new Thread(threadStopDemo).start();
Thread.sleep(1000);
for (int i = 0; i < 2000; i++) {
System.out.println("main Thread: I am counting-"+i);
if (i==1980){
threadStopDemo.stop();
}
}
}
}
3.2 线程休眠(SLEEP)
使用sleep方法使得线程休眠,指定当前线程阻塞的毫秒数。sleep时间达到后线程进入就绪状态。每个对象都有一个对象锁,sleep方法不会释放锁。
sleep方法的定义如下:
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, subject to
* the precision and accuracy of system timers and schedulers. The thread
* does not lose ownership of any monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static native void sleep(long millis) throws InterruptedException;
sleep存在异常InterruptedException,使用的时候需要处理异常。
使用sleep方法实现倒计时的功能(实际使用中应该使用定时器实现),代码示例:
public class ThreadSleepDemo implements Runnable{
SimpleDateFormat dateFormat=new SimpleDateFormat("HH:mm:ss");
@Override
public void run() {
try {
int i=0;
while (true){
if (i>10){
break;
}
Thread.sleep(1000);
System.out.println(dateFormat.format(new Date()));
i++;
}
System.out.println("神州100号发射~");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new ThreadSleepDemo()).start();
}
}
代码执行结果:
17:47:32
17:47:33
17:47:34
17:47:35
17:47:36
17:47:37
17:47:38
17:47:39
17:47:40
17:47:41
17:47:42
神州100号发射~
3.3 线程礼让(YIELD)
线程礼让是指让当前正在执行的线程暂停,但不阻塞,将线程从运行状态转化为就绪状态。让CPU重新调度线程的执行。
yield()方法的定义如下:
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
代码示例:
public class ThreadYieldDemo {
public static void main(String[] args) {
Runnable runnable=()->{
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程执行结束");
};
Thread thread01 = new Thread(runnable, "线程1");
Thread thread02 = new Thread(runnable, "线程2");
thread01.start();
thread02.start();
}
}
代码执行结果:
执行结果看CPU的调度,所以结果不固定,执行结果可能是:
线程1线程开始执行
线程1线程执行结束
线程2线程开始执行
线程2线程执行结束
或者是:
线程2线程开始执行
线程1线程开始执行
线程2线程执行结束
线程1线程执行结束
3.4 线程强制执行(JOIN)
使用join()方法,使得线程强制执行,其他线程进入阻塞状态,需要待此线程执行完成后,再执行其他线程。可以看成平时生活中的插队操作。
join方法的定义:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
join方法使用的代码示例:
public class ThreadJoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 200; i++) {
System.out.println("来了" + i + "个VIP插队");
}
},"vip线程");
thread.start();
for (int i = 0; i < 1000; i++) {
if (i==200){
//vip线程强制执行,当前main线程进入阻塞,需要等待vip线程执行完
thread.join();
}
System.out.println("main 线程:"+i);
}
}
}
代码执行结果:
main 线程:198
main 线程:199
来了104个VIP插队
//省略
来了199个VIP插队
main 线程:200
4. 线程优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程的优先级设定应该线程启动之前,线程的优先级用数字来表示,范围从1~10。通过setPriority()方法改变优先级,Thread类的相关代码如下:
public
class Thread implements Runnable {
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
线程优先级代码示例:
public class ThreadPriorityDemo {
public static void main(String[] args) {
Runnable runnable=()-> System.out.println(Thread.currentThread().getName()+":"+Thread.currentThread().getPriority());
//主线程
System.out.println("main:"+Thread.currentThread().getPriority());
Thread thread01 = new Thread(runnable);
thread01.setPriority(10);
Thread thread02 = new Thread(runnable);
thread02.setPriority(8);
Thread thread03 = new Thread(runnable);
thread03.setPriority(6);
Thread thread04 = new Thread(runnable);
thread04.setPriority(4);
thread04.start();
thread03.start();
thread02.start();
thread01.start();
}
}
代码执行结果:
main:5
Thread-0:10
Thread-1:8
Thread-2:6
Thread-3:4
可以看到main线程默认的优先级为5,虽然优先级低的线程虽然先启动,但是最后先调用的是优先级高的线程先进行运行状态。
但是优先级低只是意味着获得CPU调度的概率低,并不是优先级低一定在优先级高的线程后才被CPU调用。最终线程的调用还是取决于CPU的调度。
5. 守护线程
线程分为用户线程和守护线程。虚拟机必须确保用户线程执行完毕,但不用等待守护线程执行完毕,如后台记录操作日志,监控内存,垃圾回收的线程。
通过setDaemon()方法设置自己创建的线程为守护线程。
6. 线程同步机制
6.1 基本介绍
线程安全是指,多个线程执行一段代码,这段代码始终都能表现正确的行为,那么就可以称这段代码是线程安全的。为了保存线程安全,需要使用线程同步机制。
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块。
6.2 同步方法
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁。后面被阻塞的线程才能获得这个锁,继续执行。
使用synchronized关键字解决买票问题:
有问题的代码:
public class TicketBuy {
public static void main(String[] args) {
TicketOffice ticketOffice = new TicketOffice();
new Thread(ticketOffice,"小明").start();
new Thread(ticketOffice,"小黄").start();
new Thread(ticketOffice,"小林").start();
}
}
/**
* 售票处
*/
class TicketOffice implements Runnable{
private int ticketNum=10;
private boolean flag=true;
private void sell() throws InterruptedException {
if (ticketNum<=0){
flag=false;
return;
}
//模拟方法的延时
Thread.sleep(1000);
//成功出售了票
System.out.println(Thread.currentThread().getName()+"买到了票:"+ticketNum--);
}
@Override
public void run() {
while (flag){
try {
sell();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
代码执行结果:
小明买到了票:10
小黄买到了票:9
小林买到了票:8
小林买到了票:7
小黄买到了票:6
小明买到了票:5
小明买到了票:4
小黄买到了票:4
小林买到了票:3
小林买到了票:2
小明买到了票:0
小黄买到了票:1
可以看到结果是有问题的,因为票数出现了重复,这是因为启动的三个线程都同时访问了TicketOffice对象,导致了ticketNum的值不能及时更新。
要解决这个问题,在sell方法上加上synchronic关键字即可。线程访问sell方法就持有该方法所在对象TicketOffice的锁,其他的线程阻塞。
private synchronized void sell() throws InterruptedException {
if (ticketNum<=0){
flag=false;
return;
}
//模拟方法的延时
Thread.sleep(1000);
//成功出售了票
System.out.println(Thread.currentThread().getName()+"买到了票:"+ticketNum--);
}
同步方法的弊端:一个方法中有只读和修改的代码,方法里面只有需要修改的内容才需要锁,锁的太多,会浪费资源,影响效率。
6.3 同步静态方法
同步静态方法是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得 当 前 class 的锁。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有⼀份)。
所以,如果⼀个线程 A 调⽤⼀个实例对象的
⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,
是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访
问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。
synchronized void static method(){
}
6.4 同步块
同步块,指定加锁对象,对给定对象/类加锁。 synchronized(this/object) 表示进⼊同步代码
库前要获得给定对象的锁。 synchronized(**.class) 表示进⼊同步代码前要获得 当前 class 的锁
synchronized(obj){
//业务代码
}
银行取钱问题:
模拟两个人同时取同一个账户的钱
public class Bank {
public static void main(String[] args) {
Account account = new Account(100, "买车存款");
new Withdrawal(account,100,"丈夫").start();
new Withdrawal(account,50,"妻子").start();
}
}
/**
* 个人账户
*/
class Account{
/**
* 余额
*/
int money;
/**
* 卡名
*/
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
/**
*模拟取款的过程
*/
class Withdrawal extends Thread{
/**
* 账户
*/
private Account account;
/**
* 要取多少钱
*/
int drawMoney;
/**
* 取款人
*/
String name;
public Withdrawal(Account account,int drawMoney,String name){
super(name);
this.name=name;
this.account=account;
this.drawMoney=drawMoney;
}
@Override
public synchronized void run() {
//判断还有没钱
if (account.money-drawMoney<0){
System.out.println(name+"取不了钱,账户余额不足");
return;
}
//模拟方法延时
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡里余额:原本的钱-要取的钱
account.money=account.money-drawMoney;
System.out.println(name+"取走了"+drawMoney);
System.out.println(account.name+"当前余额:"+account.money);
}
}
代码执行结果:
丈夫取走了100
买车存款当前余额:0
妻子取走了50
买车存款当前余额:-50
可以看到在run方法上加synchronized关键字,不能达到理想的线程同步效果,这是因为synchronized默认锁的是方法所在的对象,而代码中是创建了两个线程对象访问,故在方法上不起作用。应该使用同步代码块,将run方法中加上同步代码块,锁定account对象。
@Override
public void run() {
synchronized (account){
//判断还有没钱
if (account.money-drawMoney<0){
System.out.println(name+"取不了钱,账户余额不足");
return;
}
//模拟方法延时
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡里余额:原本的钱-要取的钱
account.money=account.money-drawMoney;
System.out.println(name+"取走了"+drawMoney);
System.out.println(account.name+"当前余额:"+account.money);
}
}
代码执行结果:
丈夫取走了100
买车存款当前余额:0
妻子取不了钱,账户余额不足
7. 死锁
7.1 基本介绍
线程死锁描述的是这样一种情况:多个线程各自占有一些共享资源,并且互相都等待其他线程占有的资源才能继续运行并且不放弃自己拥有的资源,而导致多个线程都在等待对方释放资源,都停止执行的情况。
如下图所示,线程 A 持有资源 1,线程 B 持有资源 2,他们同时都想申请对⽅的资源,所以这两
个线程就会互相等待⽽进⼊死锁状态。
7.2 死锁产生条件
- 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。
- 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕
后才释放资源。 - 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。
死锁的代码示例:
public class DeadLockDemo {
/**
* 资源 1
*/
private static Object resource1 = new Object();
/**
* 资源 2
*/
private static Object resource2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
代码执行结果:
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 2,5,main]waiting get resource1
Thread[线程 1,5,main]waiting get resource2
产生死锁的原因,线程1首先获得资源1的对象监视器锁,然后进入睡眠状态,线程2获得资源2的对象监视器锁,然后进入睡眠状态。然后线程2得到CPU调度继续执行,请求获得资源1的对象监视器锁,因为线程1未释放该锁,线程2阻塞;然后线程1得到CPU调度继续执行,线程1请求获得资源2的对象监视器锁。因为线程2未释放该锁,线程1也阻塞,造成了死锁。
简单来说,线程 1和线程2都持有对方想要的资源,双方都需要彼此拥有的资源才能完成代码执行,形成了僵持状态。
7.3 破坏死锁
为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。现在我们来挨个分析⼀下:
- 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界
资源需要互斥访问)。 - 破坏请求与保持条件 :⼀次性申请所有的资源。
- 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释
放它占有的资源。 - 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。
破坏循环等待条件。
上面死锁的例子是循环等待造成的,所以需要破坏循环等待条件来破坏死锁,修改上面的死锁代码:
public class DeadLockDemo {
/**
* 资源 1
*/
private static Object resource1 = new Object();
/**
* 资源 2
*/
private static Object resource2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
}
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
}
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}, "线程 2").start();
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44305.html