线程的创建
继承Thread
类,并覆盖run()
方法
Thread的实现类继承Thread
类,并覆盖其run()
方法,run()
方法中定义线程需要执行的任务,并调用实现类的start()
方法创建线程.
调用实现类的
run()
方法只是单纯的方法调用,并不能新建线程.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("子线程启动,ID为:" + Thread.currentThread().getId() + ",名字为" + Thread.currentThread().getName());
}
}
class Test {
public static void main(String[] args) {
// 创建一个线程并开启线程
MyThread thread = new MyThread();
thread.start();
// 多创建几个线程
new MyThread().start();
new MyThread().start();
new MyThread().start();
}
}
输出如下:
子线程启动,ID为:13名字为Thread-0
子线程启动,ID为:15名字为Thread-2
子线程启动,ID为:16名字为Thread-3
子线程启动,ID为:14名字为Thread-1
实现Runnable
接口,并覆盖run()
方法
因为Java是单继承的,因此直接继承Thread
类常常并不是一个好主意.
我们常常通过实现Runnable
接口或Callable
接口定义一个任务,并将其传给Thread
类构造函数来创建一个线程.其中Runnable
类的run()
方法没有返回值,而Callable
类的call()
方法可以有返回值.
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("子线程启动,ID为:" + Thread.currentThread().getId() + ",名字为" + Thread.currentThread().getName());
}
}
class Test {
public static void main(String[] args) {
// 通过将Runnable对象传入Thread构造函数来创建线程,并开启线程
Runnable runnable = new MyRunnable();
Thread thread1 = new Thread(runnable, "线程1");
thread1.start();
// 一个Runnable对象可以用来创建多个线程
new Thread(runnable, "线程2").start();
new Thread(runnable, "线程3").start();
new Thread(runnable, "线程4").start();
}
}
输出如下:
子线程启动,ID为:14,名字为线程2
子线程启动,ID为:13,名字为线程1
子线程启动,ID为:16,名字为线程4
子线程启动,ID为:15,名字为线程3
观察Thread
类源代码,会发现Thread
类也实现了Runnable
接口.
实现Callable
接口,并覆盖call()
方法
通过该方法创建的任务可以有返回值.但Callable
对象只能传给线程池,创建线程的具体方法见后边文章.
要注意
Callable
和Runnable
定义的都是任务而不是线程,要将其传入一个线程或线程池后才可以执行.
线程的状态
线程有五个状态:
新生(new
)状态: 用new关键字建立一个线程对象后,该线程对象就处于新生状态,有自己的内存空间.
就绪(runnable
)状态: 调用了start()方法后,线程转为就绪状态,处于就绪状态线程具备了运行条件,但还没分配到CPU,等待系统CPU调度.
运行(running
)状态: 处于运行状态的线程正在执行自己的run()方法中代码.
阻塞(blocked
)状态: 线程暂停执行,让出CPU并将其交给其他线程使用.
死亡(dead
)状态: 当线程完成工作或抛出异常时,线程死亡,不再执行.
可以用厕所类比CPU来理解进程的五个状态:
- 并不是你线程一start()就能办事了,要先排队等待CPU调度,此时处于就绪状态.
- 当你的时间片用完后,就要出来重新排队,相当于受到CPU调度重新进入就绪状态.
- 当出现一些原因导致线程运行不下去(等待给送手纸),就进入阻塞状态,直到解决阻塞后有重新排队等待CPU调度,此时处于就绪状态.
线程控制基本方法
判断线程状态的方法
-
public long getId()
: 得到线程的ID -
public String getName()
和public void setName(String name):
得到或设置线程名称 -
public boolean isAlive()
: 判断当前线程是否处于活动状态
class MyThread extends Thread {
@Override
public void run() {
System.out.println("run:" + this.isAlive());
}
}
class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
System.out.println("begin:" + myThread.isAlive());
myThread.start();
System.out.println("end:" + myThread.isAlive());
}
}
begin:false
run:true
end:true
我们看到程序输出end:true
,说明再执行该句输出时,子线程还没运行完毕,实际上多运行几次程序就会发现,这个值是不确定的.
-
public int getPriority()
和public void setPriority(int newPriority)
: 得到或设置线程优先级.Java中线程的优先级为1~10之间的整数,一个线程的默认优先级为5.
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NORM_PRIORITY = 5;优先级的高低只是意味着获得调度概率的高低,并不代表调度的绝对顺序.
-
public static Thread currentThread()
返回当前代码段正在被哪个线程调用.
阻塞线程的方法
-
public void join()
,public void join(long millis)
,public void join(long millis, int nanos)
: 合并线程,调用某线程的join()方法,会将当前线程与该线程合并,即等待该线程执行结束之后再恢复当前线程的运行.join()
方法可以看作把并发的线程变为在一个线程内的函数调用class MyThread extends Thread { @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); sleep(1000); } } } class Test{ public static void main(String[] args) throws InterruptedException { // 将子线程thread1 join进主线程,则主线程会等待子线程销毁之后再执行 Thread thread1 = new MyThread(); thread1.start(); thread1.join(); // 当子线程thread1销毁之后,下边语句才会执行 // 子线程thread2并没有join进主线程,会和主线程交替执行 Thread thread2 = new MyThread(); thread2.start(); for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); Thread.sleep(1000); } } }
程序输出如下:
Thread-0:1 Thread-0:2 Thread-0:3 Thread-0:4 Thread-0:5 Thread-1:1 main:1 main:2 Thread-1:2 Thread-1:3 main:3 main:4 Thread-1:4 main:5 Thread-1:5
-
public static sleep(long millis)
,public void sleep(long millis, int nanos)
: 使线程停止运行一段时间并转入阻塞状态
.sleep()
方法不会交出锁.- 因为
sleep()
方法使线程进入阻塞状态
,因此若调用了sleep()
方法后,即使没有其他等待执行的线程,当前线程也不会马上恢复执行.
-
public static void yield()
: 礼让线程,让当前正在执行线程暂停并转入就绪状态
.yield()
方法不会交出锁.- 因为
yield()
方法使线程进入就绪状态
,因此若调用了yield()
方法后,没有其他等待执行的线程,当前线程就会马上恢复执行.
线程同步
synchronized
关键字
synchronized
关键字的意义
-
synchronized(对象)
对括号内的对象加锁,任何线程要执行synchronized
代码块中的代码,都必须要先拿到该对象的锁,当代码块执行完毕时,锁就会释放,被其他线程获取public class T { private int count = 10; private final Object lock = new Object(); // 锁对象 public void m() { synchronized (lock) { // 任何线程要执行下面的代码,都必须先拿到lock锁,锁信息记录在堆内存对象中的,不是在栈引用中 count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } // 当上述synchronized代码块执行完毕后,锁就会被释放,然后被其他线程获取 } }
-
每次使用锁都新建一个锁对象比较麻烦,因此我们可以直接对this对象加锁.
public class T { private int count = 10; public void m() { synchronized (this) { // 任何线程要执行下面的代码,必须先拿到this锁 // synchronized锁定的不是代码块,而是this对象 count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } } }
-
若整个方法内所有代码都被
synchronized
修饰,则可以使synchronized
关键字修饰整个方法.public class T { private int count = 10; public synchronized void m() { // 等同于 synchronized (this) { count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } // } }
-
若
synchronized
关键字锁定静态方法,等价于锁定T.class
对象public class T { private static int count = 10; public static synchronized void m() { // 等同于 synchronized (T.class) { count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } // 上边m()方法与下边mm()方法等价 public static synchronized void mm() { synchronized (T.class) { // 这里不能使用synchronized(this),因为静态方法不需要实例对象即可访问 count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } } }
synchronized
关键字的使用
使用synchronized
关键字修饰代码块,保证synchronized
代码块内操作的原子性
public class T implements Runnable{
private int count = 10;
@Override
public /*synchronized*/ void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
T t = new T();
for (int i = 0; i < 5; i++) {
new Thread(t).start(); //这里new的所有线程的锁住的是同一个上边的t对象
}
}
}
不加synchronized
关键字,程序输出如下: 因为不保证原子性,每个线程在执行自减操作和输出操作之间都可能被其它线程打断.
Thread-0 count = 7
Thread-4 count = 5
Thread-3 count = 6
Thread-2 count = 7
Thread-1 count = 7
加上synchronized
关键字,程序输出如下:
Thread-0 count = 9
Thread-4 count = 8
Thread-3 count = 7
Thread-2 count = 6
Thread-1 count = 5
细粒度的锁比粗粒度的锁效率高,因为锁的东西更少了。
深入理解synchronized
关键字
-
若
synchronized
修饰的代码块中出现异常,线程进行异常处理后会马上释放锁(与ReentrantLock
正相反).public class T { int i = 0; // 同步方法,计数到5抛出异常 synchronized void m() { System.out.println(Thread.currentThread().getName() + " start"); while (true) { i++; System.out.println(Thread.currentThread().getName() + ": " + i); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 计数到5抛出异常 if (i == 5) { int error = 1 / 0; } } } public static void main(String[] args) { T t = new T(); new Thread(t::m, "线程1").start(); new Thread(t::m, "线程2").start(); } }
输出如下,我们看到
线程1
抛出异常后马上释放锁,锁被线程2
抢到并开始执行.线程1 start 线程1: 1 线程1: 2 线程1: 3 线程1: 4 线程1: 5 线程2 start Exception in thread "线程1" java.lang.ArithmeticException: / by zero at T.m(T.java:20) at java.base/java.lang.Thread.run(Thread.java:835) 线程2: 6 线程2: 7 线程2: 8 线程2: 9 线程2: 10 线程2: 11
webapp中,多个servlet访问同一个资源,如果异常处理不合适。在第一个线程中抛出异常,其他线程进入同步代码块,可能访问到第一个线程处理一半的数据,导致数据不一致。因此小心处理同步业务逻辑中的异常。
如果不想释放锁——使用trycatch,捕获异常,进行处理。
try{ // 计数到5抛出异常 if (i == 5) { int error = 1 / 0; } }catch(Exception e){ System.out.println("除0了"); }
线程1 start 线程1: 1 线程1: 2 线程1: 3 线程1: 4 线程1: 5 除0了 线程1: 6 线程1: 7 线程1: 8 线程1: 9
-
synchronized
锁住的是堆中o对象的实例,而不是o对象的引用,因为synchronized
是针对堆中o对象的实例上进行计数.- 若在程序运行过程中,引用o指向对象的属性发生改变,锁状态不变.
- 若在程序运行过程中,引用o指向的对象发生改变,则锁状态改变,原本抢到的锁作废,线程会去抢新锁.
因此实际编程中常将锁对象的引用用
final
修饰,保证其指向的锁对象不发生改变.(final
修饰引用时,该引用所指向的属性可以改变,但该引用不能再指向其他对象)public class T { Object o = new Object(); // 该方法锁住的o对象引用没有被设为final void m() { synchronized (o) { while (true) { System.out.println(Thread.currentThread().getName() + "正在运行"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { T t = new T(); new Thread(t::m, "线程1").start(); // 在这里让程序睡一会儿,保证两个线程得到的o对象不同 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } Thread thread2 = new Thread(t::m, "线程2"); // 改变锁引用,使得线程2也有机会运行,否则一直都是线程1运行 t.o = new Object(); thread2.start(); } }
程序输出如下,看到主线程睡了3秒之后,
线程1
和线程2
交替运行,他们各自抢到了不同的锁.线程1正在运行 线程1正在运行 线程1正在运行 线程2正在运行 线程1正在运行 线程2正在运行 线程1正在运行 线程2正在运行 线程1正在运行 线程2正在运行 ...
如果没有改变锁引用,将会一直是线程1在运行。
-
不要以字符串常量作为锁定对象: 因为字符串常量池的存在,两个不同的字符串引用可能指向同一字符串对象
public class T { // 两个字符串常量,作为两同步方法的锁 String s1 = "Hello"; String s2 = "Hello"; // 同步m1方法以s1为锁 void m1() { synchronized (s1) { while (true) { System.out.println(Thread.currentThread().getName() + ":m1 is running"); } } } // 同步m2方法以s2为锁 void m2() { synchronized (s2) { while (true) { System.out.println(Thread.currentThread().getName() + ":m1 is running"); } } } public static void main(String[] args) { T t = new T(); // 输出两个锁的哈希码 System.out.println(t.s1.hashCode()); System.out.println(t.s2.hashCode()); new Thread(t::m1, "线程1").start(); new Thread(t::m2, "线程2").start(); } }
程序执行结果如下,我们发现两个字符串常量指向的是同一对象,且有一个线程永远得不到锁. 若我们的程序与某个库使用了同一个字符串对象作为锁,就会出现难以发现的bug.
69609650 69609650 线程1:m1 is running 线程1:m1 is running 线程1:m1 is running 线程1:m1 is running 线程1:m1 is running 线程1:m1 is running
-
synchronized
方法和非synchronized
方法是否可以同时执行?synchronized
方法和非synchronized
方法可以同时执行,因为非synchronized
方法不需要抢这把锁。public class T { // 同步方法 public synchronized void m1() { System.out.println(Thread.currentThread().getName() + " m1 start"); try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " m1 end"); } // 非同步方法 public void m2() { System.out.println(Thread.currentThread().getName() + " m2 start"); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " m2 end"); } public static void main(String[] args) { T t = new T(); new Thread(t::m1).start(); new Thread(t::m2).start(); } }
程序输出如下:
Thread-0 m1 start Thread-1 m2 start Thread-1 m2 end Thread-0 m1 end
我们发现在同步方法
m1()
执行的同时,非同步方法m2()
也在执行. -
synchronized
是可重入锁,同一线程内同步方法之间可以相互调用-
一个同步方法可以调用另外一个同步方法.若一个线程已抢到某对象的锁,再申请时仍然会得到该对象的锁. 因为这是在同一个线程以内,无非就是给锁上的数字加一.
public class T { // 一个同步方法 synchronized void m1() { System.out.println("m1 start"); m2(); // 在同步方法m1()中调用同步方法m2(),不会发生死锁,因为这是在同一线程内的调用 System.out.println("m1 end"); } // 另一个同步方法 synchronized void m2() { System.out.println("m2 start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m2 end"); } public static void main(String[] args) { T t = new T(); new Thread(t::m1).start(); } }
程序输出如下,没有发生死锁,且
m1()
方法会等待m2()
方法结束后继续运行,说明这是函数调用,而非线程并行.m1 start m2 start m2 end m1 end
-
同样的,子类的同步方法可以调用父类的同步方法也不会发生死锁,两个方法锁住的
this
指向的都是同一个子类对象
.public class T { // 父类同步方法 synchronized void m2() { System.out.println("father method start"); System.out.println("father method lock:" + this); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("father method end"); } } class TT extends T { // 子类同步方法 @Override synchronized void m1() { System.out.println("child method start"); System.out.println("child method lock:" + this); super.m(); System.out.println("child method end"); } public static void main(String[] args) { TT tt = new TT(); new Thread(tt::m1).start(); } }
程序输出结果如下,没有发生死锁,且
m1()
方法会等待m2()
方法结束后继续运行,说明这是函数调用,而非线程并行; 另外也可以看到父子的同步方法持有的是同一把锁。child method start child method lock:thread01.TT@2dd5c6ac father method start father method lock:thread01.TT@2dd5c6ac father method end child method end
-
死锁问题
多个进程在执行过程中互相等待对方的资源,导致阻塞
class Task1 implements Runnable {
@Override
public void run() {
try {
System.out.println("Task1 running");
while (true) {
synchronized (DeadLock.lock1) {
System.out.println("Task1 get lock1");
Thread.sleep(3000); //获取lock1后此线程睡眠一会,给Lock2足够的时间获得lock2
synchronized (DeadLock.lock2) {
System.out.println("Task1 get lock2");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Task2 implements Runnable {
@Override
public void run() {
try {
System.out.println("Task2 running");
while (true) {
synchronized (DeadLock.lock2) {
System.out.println("Task2 get lock2");
Thread.sleep(3000); //获取lock2后此线程睡眠一会,给Lock1足够的时间获得lock1
synchronized (DeadLock.lock1) {
System.out.println("Task2 get lock1");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class DeadLock {
// 两个不同的锁对象
public static Object lock1 = new Object();
public static Object lock2 = new Object();
public static void main(String[] args) {
// 两个线程分别等待对方已经获得的锁
Thread a = new Thread(new Task1());
Thread b = new Thread(new Task2());
a.start();
b.start();
}
}
出现死锁:
Task1 running
Task1 get lock1
Task2 running
Task2 get lock2
等待/通知(wait/notify)机制
wait()
和notify()
方法
wait()
,notify()
和notifyAll()
方法是继承自Object
的方法,都必须用在synchronized
代码块中,( 调用被锁定对象的wait()、notify()方法)其作用如下:
-
public void wait()
,public void wait(long timeoutMillis)
,public void wait(long timeoutMillis, int nanos)
: 使锁定在此对象上的线程暂停进入阻塞状态
,wait()
操作会释放锁.sleep不释放锁,wait释放锁,notify也不释放锁
-
public void notify()
: 其他线程调用,唤醒一个正在wait
在此对象上的线程. -
public void notifyAll()
: 唤醒正在wait
在此对象上的所有进程.
其中wait()
方法会释放锁,而notify()
方法不会释放锁,而要等到synchronized
代码块执行完才释放锁. 因此有时notify()
方法唤醒其它线程后要再wait()
一下释放锁,这样才有可能保证其它线程马上被唤醒.
解决方法——门闩CountdownLatch
public class CountDownLatchDemo {
volatile List lists = new ArrayList();
public void add(Object o){
lists.add(o);
}
public int size(){
return lists.size();
}
public static void main(String[] args) {
CountDownLatchDemo c = new CountDownLatchDemo();
CountDownLatch latch = new CountDownLatch(1);//1变成0的时候门闩就开了
new Thread(() -> {
System.out.println("t2启动");
if(c.size() != 5){
try{
latch.await();//等待门闩放下,不需要锁定任何对象
}catch(Exception e){
e.printStackTrace();
}
}
System.out.println("t2结束");
},"t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
for(int i = 0;i < 10;i++){
c.add(new Object());
System.out.println("add "+i);
if(c.size() == 5){
latch.countDown();//打开门闩,可以继续向下运行,不需要释放锁
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1结束");
},"t1").start();
}
}
运行结果
t2启动
t1启动
add 0
add 1
add 2
add 3
add 4
t2结束
add 5
add 6
add 7
add 8
add 9
t1结束
生产者/消费者模式
两个线程使用同一容器,使用wait()
和notify()
进行线程间通信的模式为生产者/消费者模式
// 产品类
class Product {
int id;
public Product(int id) {
this.id = id;
}
@Override
public String toString() {
return "Product{id=" + id + '}';
}
}
// 同步栈,用于在生产者线程和消费者线程之间通信
class SyncStack {
int index = 0; // 栈顶元素上一位的下标
Product[] products = new Product[4];
// 向栈中送入产品,synchronized方法保证原子性
public synchronized void push(Product product) {
// 栈满了,停止生产
while (index == products.length) {
// 为防止wait时发生异常后执行程序剩余部分或栈被其它生产者线程生产满,使用while而非if检测栈状态
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 栈不满,通知消费者线程消费
// 因为notify()方法不能指定唤醒哪个线程,若只唤醒了另一生产者线程,则会发生死锁,因此我们需要把所有线程都唤醒.
this.notifyAll();
products[index] = product;
index++;
}
// 从栈中取出产品,synchronized保证原子性
public synchronized Product pop() {
// 栈空了,停止消费
while (index == 0) {
// 为防止wait时发生异常后执行程序剩余部分或栈被其它消费者线程消费空,使用while而非if检测栈状态
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 栈不空,通知生产者线程生产
// 因为notify()方法不能指定唤醒哪个线程,若只唤醒了另一消费者线程,则会发生死锁,因此我们需要把所有线程都唤醒.
this.notifyAll();
index--;
return products[index];
}
}
// 生产者类
class Producer implements Runnable {
// 生产者工作的栈
SyncStack syncStack = null;
public Producer(SyncStack syncStack) {
this.syncStack = syncStack;
}
@Override
public void run() {
// 生产10个产品
for (int i = 0; i < 10; i++) {
Product product = new Product(i);
syncStack.push(product);
System.out.println("produce" + product);
try {
Thread.sleep((int)(Math.random()*200));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者类
class Consumer implements Runnable {
// 消费者消费的栈
SyncStack syncStack = null;
public Consumer(SyncStack syncStack) {
this.syncStack = syncStack;
}
@Override
public void run() {
// 消费10个产品
for (int i = 0; i < 10; i++) {
Product product = syncStack.pop();
System.out.println("consume" + product);
try {
Thread.sleep((int)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ProducerConsumer {
public static void main(String[] args) {
SyncStack syncStack = new SyncStack();
// 生产者和消费者都注入了同一个同步栈,线程间的锁都锁在同步栈syncStack上
Producer producer = new Producer(syncStack); // 生产者
Consumer consumer = new Consumer(syncStack); // 消费者
// 创建多个生产者线程和消费者线程
new Thread(producer, "生产者1").start(); // 生产者线程1
new Thread(producer, "生产者2").start(); // 生产者线程2
new Thread(consumer, "消费者1").start(); // 消费者线程1
new Thread(consumer, "消费者2").start(); // 消费者线程2
}
}
通过wait/notify
机制,我们可以保证生产者和消费者线程不会同时阻塞,消费者每次消费
和生产者每次生产
都会唤醒对方线程.
在写生产者/消费者模式
时,要注意下面几个问题:
-
同步栈
SyncStack
的push()
方法和pop()
方法的操作均不具有原子性,因此我们需要通过加以synchronized
修饰以保证线程同步。因为生产者和消费者注入的是同一SyncStack
对象,因此两线程竞争的是同一把锁. -
一进入
SyncStack
类的push()
方法和pop()
方法,就要判断当前栈是否满/空,以确保数组不越界.判断栈满/空要使用while
语句而非if
语句. 有以下两个原因- 在进程被唤醒期间,原本不满/不空的同步栈有可能被其它先被唤醒的生产者/消费者操作过后变满/变空.
- 一旦在
wait()
方法中出现异常,若使用if
语句,则就会直接进入catch
语句,打印异常并退出if
语句,执行后面对数组的操作
这两种情况下对数组进行操作都是危险的,因此我们要在线程被唤醒后仍然检查一次当前栈状况再操作栈.(根据Effective Java的说法,
wait()
方法在99.9%的情况下都是跟while
语句在一起的). -
在唤醒其他线程时,我们使用notifyAll()方法而非notify()方法.这是因为我们无法指定环形的是哪个线程,若只唤醒了一个与本线程同角色(生产者唤醒生产者/消费者唤醒消费者)的线程,则会发生死锁,因此我们使用notifyAll()方法唤醒所有正在等待的线程.(根据Effective Java的说法,要永远使用notifyAll(),而不使用notify()).
volatile
关键字
volatile
关键字向编译器声明该变量是易变的,每次对volatile
关键字的修改会通知给所有相关进程.
-
要理解
volatile
关键字的作用,要先理解Java内存模型JMM
- 在
JMM中
,所有对象以及信息都存放在主内存中(包含堆,栈),而每个线程在CPU中都有自己的独立空间,存储了需要用到的变量的副本. - 线程对共享变量的操作,都会先在自己CPU中的工作内存中进行,然后再同步给主内存.若不加
volatile
关键字修饰,每个线程都有可能从自己CPU中的工作内存读取内存;而加以volatile
关键字修饰后,每个线程对该变量进行修改后都会马上通知给所有进程.
public class T { /*volatile*/ boolean running = true; // 若无volatile关键字修饰,则变量running难以在每个线程之间共享,对running变量的修改自然不能终止线程 // 可以通过将running变量设为false来终止m()方法 void m() { System.out.println("m start"); while (running) { // 死循环 } System.out.println("m end"); } public static void main(String[] args) { T t = new T(); new Thread(t::m, "t1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 将running变量设为false,观察线程是否被终止 t.running = false; } }
我们发现,若不对
running
变量加以volatile
修饰,则对running变量的修改不能终止子线程,说明在主线程中对running的修改对子线程不可见.有趣的是,若在
while
死循环体中加入一些语句或sleep一段时间之后,可见性问题可能会消失,这是因为加入语句后,CPU就可能会出现空闲,并同步主内存中的内容到工作内存,但这是不确定的,因此在这种情况下还是尽量要加上volatile。 - 在
-
volatile
只能保证可见性,但不能保证原子性.volatile
不能解决多个线程同时修改一个变量带来的线程安全问题.public class T { volatile int count = 0; /*AtomicInteger count = new AtomicInteger(0);*/ /*synchronized*/ void m() { for (int i = 0; i < 10000; i++) { count++; /*count.incrementAndGet();*/ //incrementAndGet()是原子方法,而count++不是原子方法 } } public static void main(String[] args) { // 创建一个10个线程的容器,其中每个线程都执行m()方法 T t = new T(); List<Thread> threads = new ArrayList<>(); for (int i = 0; i < 10; i++) { threads.add(new Thread(t::m, "t-" + i)); } // 启动这10个线程并join到主线程,防止主线程先行结束 for (Thread thread : threads) { try { thread.start(); thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(t.count); // 10个线程,每个线程执行10000次,结果应为100000 } }
运行该程序,我们发现最终变量
t.count
并非如我们所预计的那样为100000,而是小于100000(当然,若去掉volatile
修饰,最终t.count
会更小).这说明volatile
并不能保证对变量操作的原子性.要保证多线程操作同一变量的原子性,有如下两种方法:
-
在方法上加synchronized修饰非原子操作的方法,synchronized既保证可见性,又保证原子性.但synchronized效率最低
-
使用AtomicInteger代替int类型(AtomicXXX类可以用来替代基本数据类型,其支持一些原子操作)
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
ps:单个Atomic操作是原子的,但是几句的组合是非原子的
if(count.get() < 1000)
count.incrementAndGet();
比如这两句之间可能会有别的线程进来改变值,这种情况还是要加锁
综上所述,
volatile
保证对被修饰变量的修改对于其他相关线程是可见的,即保证了可见性
;但volatile
并不能解决多个线程同时修改同一变量带来的线程安全问题,即不能保证原子性
. 因此,只有在满足以下两个条件的情况下volatile
才能保证解决线程的安全问题.- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他状态变量共同参与不变约束
-