2.3 volatile 与 Java 内存模型(JMM)
- volatile对于保证操作的原子性是由非常大的帮助的(可见性)。但是需要注意的是,volatile并不能代替锁,它也无法保证一些复合操作的原子性。比如下面的例子,通过volatile是无法保证i++的原子性操 作的:
static volatile int i = 0;
public static class PlusTask implements Runable {
@Override
public void run() {
for (int k = 0; k < 10000; k++) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new PlusTask());
threads[i].start();
}
for (int i = 0; i < 10; i++) {
threads[i].join(); //等待所有线程结束
}
System.out.println(i);
}
- 上述代码的输出总是会小于100000,由于i++不是原子性的。
- 此外,volatile也能保证数据的可见性和有序性。下面再来看一个简单的例子:
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready);
System.out.println(number);
}
}
public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
Thread.sleep(1000);
number = 42;
ready = true;
Thread.sleep(1000);
}
}
- 在虚拟机的Client模式下,由于JIT并没有做足够的优化,在主线程修改ready变量的状态后,ReaderThread可以发现这个改动,并退出程序。但是在Server模式下,由于系统优化的结果,ReaderThread线程无法“看到”主线程中的修改,导致ReaderThread永远无法退出。
- 注意:可以使用Java虚拟机参数-server切换到Server模式。
2.4 分门别类的管理:线程组
- 在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组里。
- 线程组的使用非常简单,如下:
public class ThreadGroupName implements Runnable {
public static void main(String[] args) {
ThreadGroup tg = new ThreadGroup("PrintGroup");
//使用Thread的构造函数,指定线程所属的线程组,将线程和线程组关联起来。
Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
t1.start();
t2.start();
//获得活动线程的总数,估计值。
System.out.println(tg.activeCount());
//打印这个线程组中所有的线程信息。
tg.list();
}
@Override
public void run() {
String groupAndName = Thread.currentThread().getThreadGroup().getName() + "-" + Thread.currentThread().getName();
while (true) {
System.out.println("I am " + groupAndName);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStaceTrace();
}
}
}
}
2.5 驻守后台:守护线程(Daemon)
- 守护线程是一种特殊的线程,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。
- 与之对应的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作。
- 当一个java应用内,只有守护线程时,Java虚拟机就会自然退出。
- 下面简单地看一下守护线程的使用:
public class DaemonDemo {
public static class DaemonT extends Thread {
public void run() {
while (true) {
System.out.println("I am alive");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t = new DaemonT();
t.setDaemon(true); //将线程t设置为守护线程。必须在start()之前设置。
t.start();
Thread.sleep(2000);
}
}
- 由于t被设置为守护线程,系统中只有主线程main为用户线程,因此在main线程休眠2秒后退出时,整个程序也随之结束。但如果不把线程t设置为守护线程,main线程结束后,t线程还会不停地打印,永远不会结束。
2.6 先干重要的事:线程优先级
- 在Java中,使用1到10表示线程优先级。一般可以使用内置的三个静态标量表示:
public final static int MIN_PRIORITY = 1;
public final static int MIN_PRIORITY = 5;
public final static int MIN_PRIORITY = 10;
- 数字越大则优先级越高,但有效范围在1到10之间。
- 下面的代码展示了优先级的作用。高优先级的线程倾向于更快地完成。
public class PriorityDemo {
public static class HightPriority extends Thread {
static int count = 0;
public void run() {
while (true) {
synchronized (PriorityDemo.class) {
count++;
if (count > 10000000) {
System.out.println("HightPriority is complete");
break;
}
}
}
}
}
public static class LowPriority extends Thread {
static int count = 0;
public void run() {
while (true) {
synchronized (PriorityDemo.class) {
count++;
if (count > 10000000) {
System.out.println("LowPriority is complete");
break;
}
}
}
}
}
public static void main(String[] args) {
Thread high = new HightPriority();
LowPriority low = new new LowPriority();
high.setPriority(Thread.MAX_PRIORITY);
low.setPriority(Thread.MIN_PRIORITY);
low.start();
high.start();
}
}
- 在对count累加前,我们使用synchronized产生了一次资源竞争。目的是使得优先级的差异表现得更为明显。
2.7 线程安全的概念与synchronized
- 关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。
- 关键字synchronized可以有多种用法。
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
- 下述代码,将synchronized作用于一个给定对象instance,因此,每次当线程进入被synchronized包裹的代码段,就都会要求请求instance实例的锁。如果当前有其他线程正持有这把锁,那么新到的线程就必须等待。这样,就保证了每次只能有一个线程执行i++操 作。
public class AccountingSync implements Runnable {
static AccountingSync instance = new AccountingSync();
static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
synchronized(instance) {
i++;
}
}
}
}
public class AccountingSync2 implements Runnable {
static AccountingSync2 instance = new AccountingSync2();
static int i = 0;
public synchronized void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
increase();
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
- 上述代码中,synchronized关键字作用于一个实例方法。这就是说在进入increase()方法前,线程必须获得当前对象实例的锁。在本例中就是instance对象。这里使用Runnable接口创建两个线程,并且这两个线程都指向同一个Runnable接口实例(instance)这样才能保证两个线程在工作时,能够关注到同一对象锁上去,从而保证线程安全。
- 一种错误的同步方式如下:
public class AccountingSyncBad implements Runnable {
static int i = 0;
public synchronized void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
increase();
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new AccountingSyncBad());
Thread t2 = new Thread(new AccountingSyncBad());
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
- 上述代码中,这两个线程的Runnable实例并不是同一个对象。使用的是两把不同的锁。因此,线程安全是无法保证的。
- 但是我们只要简单地修改上述代码,就能使其正确执行。那就是使用synchronized的第三种用法,将其作用于静态方法。
public static synchronized void increase() {
i++;
}
- 这样即使两个线程指向不同的Runable对象,但由于方法块需要请求的是当前类的锁,而非当前实例,因此,线程间还是可以正确同步。
- 除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,synchronized可以完全替代volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题自然得到了解决。