volatile
Java内存模型都是围绕着原子性、有序性和可见性展开的,为了在适当的场合,确保线程间的有序性、可见性和原子性。Java使用了一些特殊的操作或者关键字来申明、告诉虚拟机,在这个地方,要尤其注意,不能随意变动优化目标指令。关键字volatile就是其中之一,用于保持线程之间的。
但是同时应该注意的是,volatile并不能当锁用,他无法保证共享变量操作的原子性。比如下面的例子,通过volatile是无法保证i++的原子性操作的:
01 static volatile int i=0;
02 public static class PlusTask implements Runnable{
03 @Override
04 public void run() {
05 for(int k=0;k<10000;k++)
06 i++;
07 }
08 }
09
10 public static void main(String[] args) throws InterruptedException {
11 Thread[] threads=new Thread[10];
12 for(int i=0;i<10;i++){
13 threads[i]=new Thread(new PlusTask());
14 threads[i].start();
15 }
16 for(int i=0;i<10;i++){
17 threads[i].join();
18 }
19
20 System.out.println(i);
21 }
执行上述代码,如果第6行i++是原子性的,那么最终的值应该是100000(10个线程各累加10000次)。但实际上,上述代码的输出总是会小于100000。
重点
线程组
可以通过线程组对不同的线程分批管理
比如:
01 public class ThreadGroupName implements Runnable {
02 public static void main(String[] args) {
03 ThreadGroup tg = new ThreadGroup("PrintGroup");
04 Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
05 Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
06 t1.start();
07 t2.start();
08 System.out.println(tg.activeCount());
09 tg.list();
10 }
11
12 @Override
13 public void run() {
14 String groupAndName=Thread.currentThread().getThreadGroup().getName()
15 + "-" + Thread.currentThread().getName();
16 while (true) {
17 System.out.println("I am " + groupAndName);
18 try {
19 Thread.sleep(3000);
20 } catch (InterruptedException e) {
21 e.printStackTrace();
22 }
23 }
24 }
25 }
通过创建一个名为“PrintGroup”的线程组,并将T1和T2两个线程加入这个组中。第8、9两行,展示了线程组的两个重要的功能,activeCount()可以获得活动线程的总数,但由于线程是动态的,因此这个值只是一个估计值,无法确定精确,list()方法可以打印这个线程组中所有的线程信息,对调试有一定帮助。代码中第4、5两行创建了两个线程,使用Thread的构造函数,指定线程所属的线程组,将线程和线程组关联起来。
线程组还有一个值得注意的方法stop(),它会停止线程组中所有的线程。这看起来是一个很方便的功能,但是它会遇到和Thread.stop()相同的问题(终止业务逻辑没有处理完的线程),因此使用时也需要格外谨慎。
重点
通过ThreadGroup为线程分组,并且为之按照业务需要命名,让线程的名字更有意义,慎用线程组的stop(),这会导致破坏线程业务逻辑的原子性, 线程组的activeCount()可以获得活动线程的总数,并且可以通过list()方法可以打印这个线程组中所有的线程信息,对调试有一定帮助
守护线程(Daemon)
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。,它会完成这个程序应该要完成的业务操作。。守护线程要守护的对象已经不存在了,那么整个应用程序就自然应该结束。因此,。
01 public class DaemonDemo {
02 public static class DaemonT extends Thread{
03 public void run(){
04 while(true){
05 System.out.println("I am alive");
06 try {
07 Thread.sleep(1000);
08 } catch (InterruptedException e) {
09 e.printStackTrace();
10 }
11 }
12 }
13 }
14 public static void main(String[] args) throws InterruptedException {
15 Thread t=new DaemonT();
16 t.setDaemon(true);
17 t.start();
18
19 Thread.sleep(2000);
20 }
21 }
上述代码第16行,将线程t设置为守护线程。这里注意,,否则你会得到一个类似以下的异常,告诉你守护线程设置失败。但是你的程序和线程依然可以正常执行。只是被当做用户线程而已。因此,如果不小心忽略了下面的异常信息,你就很可能察觉不到这个错误。
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.setDaemon(Thread.java:1367)
at geym.conc.ch2.daemon.DaemonDemo.main(DaemonDemo.java:20)
在这个例子中,由于t被设置为守护线程,系统中只有主线程main为用户线程,因此在main线程休眠2秒后退出时,整个程序也随之结束。但如果不把线程t设置为守护线程,main线程结束后,t线程还会不停地打印,永远不会结束。
重点
设置守护线程必须在线程start()之前设置,否则会得到以下异常,导致守护线程失效,但是不影响主线程执行。
java.lang.IllegalThreadStateException
线程优先级
在Java中,使用1到10表示线程优先级。一般可以使用内置的三个静态标量表示:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 1
数字越大则优先级越高,但有效范围在1到10之间。下面的代码展示了优先级的作用。高优先级的线程倾向于更快地完成。
01 public class PriorityDemo {
02 public static class HightPriority extends Thread{
03 static int count=0;
04 public void run(){
05 while(true){
06 synchronized(PriorityDemo.class){
07 count++;
08 if(count>10000000){
09 System.out.println("HightPriority is complete");
10 break;
11 }
12 }
13 }
14 }
15 }
16 public static class LowPriority extends Thread{
17 static int count=0;
18 public void run(){
19 while(true){
20 synchronized(PriorityDemo.class){
21 count++;
22 if(count>10000000){
23 System.out.println("LowPriority is complete");
24 break;
25 }
26 }
27 }
28 }
29 }
30
31 public static void main(String[] args) throws InterruptedException {
32 Thread high=new HightPriority();
33 LowPriority low=new LowPriority();
34 high.setPriority(Thread.MAX_PRIORITY);
35 low.setPriority(Thread.MIN_PRIORITY);
36 low.start();
37 high.start();
38 }
39 }
上述代码定义两个线程,分别为HightPriority设置为高优先级,LowPriority为低优先级。让它们完成相同的工作,也就是把count从0加到10000000。完成后,打印信息给一个提示,这样我们就知道谁先完成工作了。这里要注意,在对count累加前,我们使用synchronized产生了一次资源竞争。目的是使得优先级的差异表现得更为明显。
大家可以尝试执行上述代码,可以看到,高优先级的线程在大部分情况下,都会首先完成任务(就这段代码而言,试运行多次,HightPriority总是比LowPriority快,但这不能保证在所有情况下,一定都是这样)。
重点
可以通过通过setPriority()设置线程的优先级,从0到10,优先级越高越容易抢占资源,但这不是绝对的,只是说理论上抢占共享资源的几率更高。
synchronized
Java 内置的管程方案(synchronized)使用简单,synchronized 关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而 Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。
并发编程里两大核心问题——互斥和同步,都可以由管程来帮你解决。学好管程,理论上所有的并发问题你都可以解决,并且很多并发工具类底层都是管程实现的,所以学好管程,就是相当于掌握了一把并发编程的万能钥匙。
关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性
synchronized的多种用法
- 指定加锁对象:
- 对给定,进入同步代码前要获得给定对象的锁。
- 直接作用于实例方法:
- 相当于对当前,进入同步代码前要获得当前实例的锁。
- 直接作用于静态方法:
- 相当于对当前,进入同步代码前要获得当前类的锁。
以上是原书给的分类,但是我感觉可以按照锁对象这个维度来看synchronized的用法:
锁对象
锁实例对象
1.修饰普通方法,锁的是this
image.png此时锁对象是this,该类的实例对象。
如果实例对象不是一个,那么保证不了共享资源的安全。
2.synchronized(this)代码块
image.png类锁,锁的是内存中唯一存在的class对象
1.修饰静态方法
image.png2.synchronized(*.class)代码块
image.pngsynchronized的配套使用
image.png image.pngsynchronized的使用例子
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++;
}
}
}
当然,上述代码也可以写成如下形式,两者是等价的:
01 public class AccountingSync2 implements Runnable{
02 static AccountingSync2 instance=new AccountingSync2();
03 static int i=0;
04 public synchronized void increase(){
05 i++;
06 }
07 @Override
08 public void run() {
09 for(int j=0;j<10000000;j++){
10 increase();
11 }
12 }
13 public static void main(String[] args) throws InterruptedException {
14 Thread t1=new Thread(instance);//锁的是当前实例,但是两个线程是同一个锁对象
15 Thread t2=new Thread(instance);
16 t1.start();t2.start();
17 t1.join();t2.join();
18 System.out.println(i);
19 }
20 }
上述代码中,synchronized关键字作用于一个实例方法。这就是说在进入increase()方法前,线程必须获得当前对象实例的锁。在本例中就是instance对象。在这里,我不厌其烦地再次给出main函数的实现,是希望强调第14、15行代码,也就是Thread的创建方式。这里使用Runnable接口创建两个线程,并且这两个线程都指向同一个Runnable接口实例(instance对象),这样才能保证两个线程在工作时,能够关注到同一个对象锁上去,从而保证线程安全。
一种错误的同步方式如下
01 public class AccountingSyncBad implements Runnable{
02 static int i=0;
03 public synchronized void increase(){
04 i++;
05 }
06 @Override
07 public void run() {
08 for(int j=0;j<10000000;j++){
09 increase();
10 }
11 }
12 public static void main(String[] args) throws InterruptedException {
13 Thread t1=new Thread(new AccountingSyncBad());
14 Thread t2=new Thread(new AccountingSyncBad());
15 t1.start();t2.start();
16 t1.join();t2.join();
17 System.out.println(i);
18 }
19 }
上述代码就犯了一个严重的错误。虽然在第3行的increase()方法中,申明这是一个同步方法。但是当锁对象是当前实例,两个线程分别分配了不同实例,导致锁对象不同,不是一把锁,线程安全无法保证。
但我们只要简单地修改上述代码,就能使其正确执行。那就是使用synchronized的第三种用法,将其作用于静态方法。将increase()方法修改如下:
public static synchronized void increase(){
i++;
除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,synchronized可以完全替代volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题自然得到了解决(换言之,被synchronized限制的多个线程是串行执行的)。
notice:
什么是管程
所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。
Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程更容易使用,所以 Java 选择了管程。
管程,对应的英文是 Monitor,而不是直译为“监视器”。
如果对管程这种技术思想有兴趣,可以继续看我的关于这块的笔记。
网友评论