起因:写这篇文章的起因就是上课没听,下课不学导致考试傻眼后(虽然最后神奇的做对了)痛定思痛要补习一下orz
补充一句:这是上学期的学习笔记。。。
目录
多线程基础
-
多任务:同时运行多个任务 操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行
-
进程:一个任务被称为一个进程(如音乐播放器和word都是进程) 线程:进程内部同时执行多个子任务,子任务被称为线程
(操作系统调度的最小任务单位) 一个进程可以包含一个或多个线程,但至少会有一个线程
┌──────────┐ │Process │ │┌────────┐│ ┌──────────┐││ Thread ││┌──────────┐ │Process ││└────────┘││Process │ │┌────────┐││┌────────┐││┌────────┐│ ┌──────────┐││ Thread ││││ Thread ││││ Thread ││ │Process ││└────────┘││└────────┘││└────────┘│ │┌────────┐││┌────────┐││┌────────┐││┌────────┐│ ││ Thread ││││ Thread ││││ Thread ││││ Thread ││ │└────────┘││└────────┘││└────────┘││└────────┘│ └──────────┘└──────────┘└──────────┘└──────────┘ ┌──────────────────────────────────────────────┐ │ Operating System │ └──────────────────────────────────────────────┘
-
实现多任务的方法,有以下几种:
多进程模式(每个进程只有一个线程):
┌──────────┐ ┌──────────┐ ┌──────────┐ │Process │ │Process │ │Process │ │┌────────┐│ │┌────────┐│ │┌────────┐│ ││ Thread ││ ││ Thread ││ ││ Thread ││ │└────────┘│ │└────────┘│ │└────────┘│ └──────────┘ └──────────┘ └──────────┘
多线程模式(一个进程有多个线程):
┌────────────────────┐ │Process │ │┌────────┐┌────────┐│ ││ Thread ││ Thread ││ │└────────┘└────────┘│ │┌────────┐┌────────┐│ ││ Thread ││ Thread ││ │└────────┘└────────┘│ └────────────────────┘
多进程+多线程模式(复杂度最高):
┌──────────┐┌──────────┐┌──────────┐ │Process ││Process ││Process │ │┌────────┐││┌────────┐││┌────────┐│ ││ Thread ││││ Thread ││││ Thread ││ │└────────┘││└────────┘││└────────┘│ │┌────────┐││┌────────┐││┌────────┐│ ││ Thread ││││ Thread ││││ Thread ││ │└────────┘││└────────┘││└────────┘│ └──────────┘└──────────┘└──────────┘
-
多进程的缺点:
-
创建进程比创建线程开销大(尤其在Windows系统上)
-
进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快
-
-
多进程的优点:
稳定性更高,一个进程崩溃不会影响其他进程,而任何一个线程崩溃会直接导致整个进程崩溃
-
Java多线程
Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行
main()
方法,在main()
方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。
和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
Java多线程编程的特点又在于:
-
多线程模型是Java程序最基本的并发模型;
-
后续读写网络、数据库、Web开发等都依赖Java多线程模型。
因此,必须掌握Java多线程编程才能继续深入学习其他内容。
-
创建新线程
-
Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行
main()
方法。在main()
方法中,我们又可以启动其他线程。 -
//要创建一个新线程非常容易,我们需要实例化一个Thread实例,然后调用它的start()方法:
public class Main { public static void main(String[] args) { Thread t = new Thread(); t.start(); // 启动新线程 } }
-
用新线程执行指定的代码
//方法一:从Thread派生一个自定义类,然后覆写run()方法: public class Main { public static void main(String[] args) { Thread t = new MyThread(); t.start(); // 启动新线程 } } class MyThread extends Thread { @Override public void run() { System.out.println("start new thread!"); } } //执行上述代码,注意到start()方法会在内部自动调用实例的run()方法。 //方法二:创建Thread实例时,传入一个Runnable实例: public class Main { public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); t.start(); // 启动新线程 } } class MyRunnable implements Runnable { @Override public void run() { System.out.println("start new thread!"); } } //或者用Java8引入的lambda语法进一步简写为: public class Main { public static void main(String[] args) { Thread t = new Thread(() -> { System.out.println("start new thread!"); }); t.start(); // 启动新线程 } }
-
public class Main { public static void main(String[] args) { System.out.println("main start..."); Thread t = new Thread() { public void run() { System.out.println("thread run..."); System.out.println("thread end."); } }; t.start(); System.out.println("main end..."); } }
主线程,也就是
main
线程,main
线程执行的代码有4行,首先打印main start
,然后创建Thread
对象,紧接着调用start()
启动新线程。当start()
方法被调用时,JVM就创建了一个新线程,我们通过实例变量t
来表示这个新线程对象,并开始执行。接着,
main
线程继续执行打印main end
语句,而t
线程在main
线程执行的同时会并发执行,打印thread run
和thread end
语句。当
run()
方法结束时,新线程就结束了。而main()
方法结束时,主线程也结束了。我们再来看线程的执行顺序:
-
main
线程肯定是先打印main start
,再打印main end
; -
t
线程肯定是先打印thread run
,再打印thread end
。
但是,除了可以肯定,
main start
会先打印外,main end
打印在thread run
之前、thread end
之后或者之间,都无法确定。因为从t
线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序。 -
-
//要模拟并发执行的效果,我们可以在线程中调用Thread.sleep(),强迫当前线程暂停一段时间:
public class Main { public static void main(String[] args) { System.out.println("main start..."); Thread t = new Thread() { public void run() { System.out.println("thread run..."); try { Thread.sleep(10); } catch (InterruptedException e) {} System.out.println("thread end."); } }; t.start(); try { Thread.sleep(20); } catch (InterruptedException e) {} System.out.println("main end..."); } } //sleep()传入的参数是毫秒。调整暂停时间的大小,我们可以看到main线程和t线程执行的先后顺序。
-
要特别注意:直接调用
Thread
实例的run()
方法是无效的:直接调用run()
方法,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。上述代码实际上是在main()
方法内部又调用了run()
方法,打印hello
语句是在main
线程中执行的,没有任何新线程被创建。必须调用
Thread
实例的start()
方法才能启动新线程,如果我们查看Thread
类的源代码,会看到start()
方法内部调用了一个private native void start0()
方法,native
修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。 -
线程的优先级
//可以对线程设定优先级,设定优先级的方法是: Thread.setPriority(int n) // 1~10, 默认值5 //优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
线程的状态
-
在Java程序中,一个线程对象只能调用一次
start()
方法启动新线程,并在新线程中执行run()
方法。一旦run()
方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:-
New:新创建的线程,尚未执行;
-
Runnable:运行中的线程,正在执行
run()
方法的Java代码; -
Blocked:运行中的线程,因为某些操作被阻塞而挂起;
-
Waiting:运行中的线程,因为某些操作在等待中;
-
Timed Waiting:运行中的线程,因为执行
sleep()
方法正在计时等待; -
Terminated:线程已终止,因为
run()
方法执行完毕。
用一个状态转移图表示如下:
┌─────────────┐ │ New │ └─────────────┘ │ ▼ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌─────────────┐ ┌─────────────┐ ││ Runnable │ │ Blocked ││ └─────────────┘ └─────────────┘ │┌─────────────┐ ┌─────────────┐│ │ Waiting │ │Timed Waiting│ │└─────────────┘ └─────────────┘│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ▼ ┌─────────────┐ │ Terminated │ └─────────────┘
当线程启动后,它可以在
Runnable
、Blocked
、Waiting
和Timed Waiting
这几个状态之间切换,直到最后变成Terminated
状态,线程终止。线程终止的原因有:
-
线程正常终止:
run()
方法执行到return
语句返回; -
线程意外终止:
run()
方法因为未捕获的异常导致线程终止; -
对某个线程的
Thread
实例调用stop()
方法强制终止(强烈不推荐使用)。
-
-
//一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:
public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { System.out.println("hello"); }); System.out.println("start"); t.start(); t.join(); System.out.println("end"); } } /* 当main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代 码打印顺序可以肯定是main线程先打印start,t线程再打印hello,main线程最后再打印end。 如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。 */
中断线程
-
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行
run()
方法,使得自身线程能立刻结束运行。我们举个栗子:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
-
中断一个线程非常简单,只需要在其他线程中对目标线程调用
interrupt()
方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1); // 暂停1毫秒 t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 System.out.println("end"); } } class MyThread extends Thread { public void run() { int n = 0; while (! isInterrupted()) { n ++; System.out.println(n + " hello!"); } } }
仔细看上述代码,
main
线程通过调用t.interrupt()
方法中断t
线程,但是要注意,interrupt()
方法仅仅向t
线程发出了“中断请求”,至于t
线程是否能立刻响应,要看具体代码。而t
线程的while
循环会检测isInterrupted()
,所以上述代码能正确响应interrupt()
请求,使得自身立刻结束运行run()
方法。 -
如果线程处于等待状态,例如,
t.join()
会让main
线程进入等待状态,此时,如果对main
线程调用interrupt()
,join()
方法会立刻抛出InterruptedException
,因此,目标线程只要捕获到join()
方法抛出的InterruptedException
,就说明有其他线程对其调用了interrupt()
方法,通常情况下该线程应该立刻结束运行。public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 System.out.println("end"); } } class MyThread extends Thread { public void run() { Thread hello = new HelloThread(); hello.start(); // 启动hello线程 try { hello.join(); // 等待hello线程结束 } catch (InterruptedException e) { System.out.println("interrupted!"); } hello.interrupt(); } } class HelloThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; System.out.println(n + " hello!"); try { Thread.sleep(100); } catch (InterruptedException e) { break; } } } }
main
线程通过调用t.interrupt()
从而通知t
线程中断,而此时t
线程正位于hello.join()
的等待中,此方法会立刻结束等待并抛出InterruptedException
。由于我们在t
线程中捕获了InterruptedException
,因此,就可以准备结束该线程。在t
线程结束前,对hello
线程也进行了interrupt()
调用通知其中断。如果去掉这一行代码,可以发现hello
线程仍然会继续运行,且JVM不会退出。 -
另一个常用的中断线程的方法是设置标志位。我们通常会用一个
running
标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running
置为false
,就可以让线程结束:public class Main { public static void main(String[] args) throws InterruptedException { HelloThread t = new HelloThread(); t.start(); Thread.sleep(1); t.running = false; // 标志位置为false } } class HelloThread extends Thread { public volatile boolean running = true; //线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。 public void run() { int n = 0; while (running) { n ++; System.out.println(n + " hello!"); } System.out.println("end!"); } } //注意到HelloThread的标志位boolean running是一个线程间共享的变量。 //线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。
为什么要对线程间共享的变量用关键字
volatile
声明?这涉及到 Java 的内存模型。在 Java 虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ Main Memory │ │ ┌───────┐┌───────┐┌───────┐ │ │ var A ││ var B ││ var C │ │ └───────┘└───────┘└───────┘ │ │ ▲ │ ▲ │ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ │ │ │ │ ┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐ ▼ │ ▼ │ │ ┌───────┐ │ │ ┌───────┐ │ │ var A │ │ var C │ │ └───────┘ │ │ └───────┘ │ Thread 1 Thread 2 └ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘
这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量
a = true
,线程1执行a = false
时,它在此刻仅仅是把变量a
的副本变成了false
,主内存的变量a
还是true
,在 JVM 把修改后的a
回写到主内存之前,其他线程读取到的a
的值仍然是true
,这就造成了多线程之间共享的变量不一致。因此,
volatile
关键字的目的是告诉虚拟机:-
每次访问变量时,总是获取主内存的最新值;
-
每次修改变量后,立刻回写到主内存。
volatile
关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。如果我们去掉
volatile
关键字,运行上述程序,发现效果和带volatile
差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。 -
守护线程
-
Java程序入口就是由JVM启动
main
线程,main
线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。如果有一个线程没有退出,JVM进程就不会退出。
所以,必须保证所有线程都能及时结束。但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:
class TimerThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(LocalTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}
如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?
然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?
答案是使用守护线程(Daemon Thread)。
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
因此,JVM退出时,不必关心守护线程是否已结束。
如何创建守护线程呢?方法和普通线程一样,只是在调用start()
方法前,调用setDaemon(true)
把该线程标记为守护线程:
Thread t = new MyThread();
t.setDaemon(true);
t.start();
在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
线程同步
-
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。
public class Main { public static void main(String[] args) throws Exception { var add = new AddThread(); var dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); } } class Counter { public static int count = 0; } class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { Counter.count += 1; } } } class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { Counter.count -= 1; } } }
上面的代码很简单,两个线程同时对一个
int
变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
例如,对于语句:
n = n + 1;
看上去是一行语句,实际上对应了3条指令:
ILOAD IADD ISTORE
我们假设
n
的值是100
,如果两个线程同时执行n = n + 1
,得到的结果很可能不是102
,而是101
,原因在于:┌───────┐ ┌───────┐ │Thread1│ │Thread2│ └───┬───┘ └───┬───┘ │ │ │ILOAD (100) │ │ │ILOAD (100) │ │IADD │ │ISTORE (101) │IADD │ │ISTORE (101)│ ▼ ▼
如果线程1在执行
ILOAD
后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD
后获取的值仍然是100
,最终结果被两个线程的ISTORE
写入后变成了101
,而不是期待的102
。这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:
┌───────┐ ┌───────┐ │Thread1│ │Thread2│ └───┬───┘ └───┬───┘ │ │ │-- lock -- │ │ILOAD (100) │ │IADD │ │ISTORE (101) │ │-- unlock -- │ │ │-- lock -- │ │ILOAD (101) │ │IADD │ │ISTORE (102) │ │-- unlock -- ▼ ▼
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用
synchronized
关键字对一个对象进行加锁:synchronized(lock) { n = n + 1; }
synchronized
保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized
改写如下:
public class Main {
public static void main(String[] args) throws Exception {
AddThread add = new AddThread();
DecThread dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}
class Counter {
public static final Object lock = new Object();
public static int count = 0;
}
class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized (Counter.lock){Counter.count += 1;}
}
}
}
class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized (Counter.lock){Counter.count -= 1;}
}
}
}
注意到代码:
synchronized(Counter.lock) { // 获取锁
...
} // 释放锁
它表示用Counter.lock
实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }
代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized
语句块结束会自动释放锁。这样一来,对Counter.count
变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0。
使用synchronized
解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized
代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized
会降低程序的执行效率。
我们来概括一下如何使用synchronized
:
-
找出修改共享变量的线程代码块;
-
选择一个共享实例作为锁;
-
使用
synchronized(lockObject) { ... }
。
在使用synchronized
的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized
结束处正确释放锁:
public void add(int m) {
synchronized (obj) {
if (m < 0) {
throw new RuntimeException();
}
this.value += m;
} // 无论有无异常,都会在此释放锁
}
public class Main {
public static void main(String[] args) throws Exception {
var ts = new Thread[] { new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread() };
for (var t : ts) {
t.start();
}
for (var t : ts) {
t.join();
}
System.out.println(Counter.studentCount);
System.out.println(Counter.teacherCount);
}
}
class Counter {
public static final Object lock = new Object();
public static int studentCount = 0;
public static int teacherCount = 0;
}
class AddStudentThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.studentCount += 1;
}
}
}
}
class DecStudentThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.studentCount -= 1;
}
}
}
}
class AddTeacherThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.teacherCount += 1;
}
}
}
}
class DecTeacherThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.teacherCount -= 1;
}
}
}
}
上述代码的4个线程对两个共享变量分别进行读写操作,但是使用的锁都是Counter.lock
这一个对象,这就造成了原本可以并发执行的Counter.studentCount += 1
和Counter.teacherCount += 1
,现在无法并发执行了,执行效率大大降低。实际上,需要同步的线程可以分成两组:AddStudentThread
和DecStudentThread
,AddTeacherThread
和DecTeacherThread
,组之间不存在竞争,因此,应该使用两个不同的锁,即:
AddStudentThread
和DecStudentThread
使用lockStudent
锁:
synchronized(Counter.lockStudent) {
...
}
AddTeacherThread
和DecTeacherThread
使用lockTeacher
锁:
synchronized(Counter.lockTeacher) {
...
}
这样才能最大化地提高执行效率。
不需要synchronized的操作
JVM规范定义了几种原子操作:
-
基本类型(
long
和double
除外)赋值,例如:int n = m
; -
引用类型赋值,例如:
List<String> list = anotherList
。
long
和double
是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long
和double
的赋值作为原子操作实现的。
单条原子操作的语句不需要同步。例如:
public void set(int m) {
synchronized(lock) {
this.value = m;
}
}
就不需要同步。
对引用也是类似。例如:
public void set(String s) {
this.value = s;
}
上述赋值语句并不需要同步。
但是,如果是多行赋值语句,就必须保证是同步操作,例如:
class Pair {
int first;
int last;
public void set(int first, int last) {
synchronized(this) {
this.first = first;
this.last = last;
}
}
}
有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。例如,上述代码如果改造成:
class Pair {
int[] pair;
public void set(int first, int last) {
int[] ps = new int[] { first, last };
this.pair = ps;
}
}
就不再需要同步,因为this.pair = ps
是引用赋值的原子操作。而语句:
int[] ps = new int[] { first, last };
这里的ps
是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。