Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java基础—线程安全与锁 #37

Open
johnnian opened this issue Sep 30, 2017 · 0 comments
Open

Java基础—线程安全与锁 #37

johnnian opened this issue Sep 30, 2017 · 0 comments
Labels

Comments

@johnnian
Copy link
Owner

johnnian commented Sep 30, 2017

一、定义

“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的”。

一个线程安全的代码,要有这样的特征:

代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己实现任何措施来保证多线程的正确调用。

二、线程间共享数据类型

2.1 不可变

不可变的对象一定是线程安全的,如 使用 final 关键字修饰的变量、String类型对象、枚举对象等;

2.2 绝对线程安全

在Java中标注自己是线程安全的类,如Vector、HashTable等,大多数都不是绝对的线程安全,可以运行下面测试代码:

package com.johnnian.thread;

import java.util.Vector;

public class ThreadDemo {
	
	private static Vector< Integer> vector = new Vector< Integer>();
	public static void main(String[] args)  {
		 while (true) { 
			 try {
				 for (int i = 0; i < 100; i++) { 
			            vector. add( i); 
			        } 
			        Thread removeThread = new Thread( new Runnable() {
			            @Override 
			            public void run() { 
			                for (int i = 0; i < vector. size(); i++) { 
			                    vector. remove( i); 
			                } 
			            } 
			        });
			        Thread printThread = new Thread( new Runnable() { 
			            @Override 
			            public void run() { 
			                for (int i = 0; i < vector. size(); i++) { 
			                	Integer item = vector. get(i);
			                } 
			            } 
			        });
			        removeThread.start(); 
			        printThread.start(); 
				} catch (Exception e) {
					// TODO: handle exception
					System.out.println(e);
				}		
		    } 
	}   
}

运行结果:

Exception in thread "Thread-23" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 141
	at java.util.Vector.get(Vector.java:748)
	at com.johnnian.thread.ThreadDemo$2.run(ThreadDemo.java:30)
	at java.lang.Thread.run(Thread.java:745)

如果对vector对象进行同步操作,修改代码如下:

Thread removeThread = new Thread( new Runnable() {
    @Override 
    public void run() { 
    	synchronized (vector) {
    		for (int i = 0; i < vector. size(); i++) { 
                vector. remove( i); 
            } 
		}
    } 
});
    
Thread printThread = new Thread( new Runnable() { 
    @Override 
    public void run() { 
    	synchronized (vector) {
            for (int i = 0; i < vector. size(); i++) { 
            	Integer item = vector. get(i);
            } 
    	}
    } 
});

结果一切正常, :)

2.3 相对线程安全

相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

2.4 线程兼容和线程对立

线程兼容指的是,原本不是线程安全的,例如 HashTable,通过一些同步方法(同步锁),保证线程安全。

线程对立指的是,无论如何都无法在多线程环境中使用,例如 Thread.suspend() & Thread.resume()方法。

三、线程安全的方法

3.1 同步互斥(阻塞同步)

方法一: 使用synchronized关键字

在Java里面,最基本的互斥同步手段就是synchronized关键字。

synchronized关键字,编译后,在同步块的前后生成 monitorentermonitorexit 这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。

monitorenter 指令(reference参数):锁计数器 +1,阻塞其他线程
代码块...
monitorexit 指令(reference参数):锁计数器 -1

reference参数:

  • synchronized指定对象, reference=对象
  • synchronized指定实例/类方法, reference=实例/类方法

方法二:ReentrantLock(重入锁)

可以用 java.util.concurrent 中的ReentrantLock实现, ReentrantLock是API级别的互斥锁,synchronized是系统级别(Java中重量级操作)。

ReentrantLock可以实现下面三种策略的锁:

名字 说明
等待可中断 持有锁的线程长时间不释放锁, 等待线程可以放弃等待,去做其他事情
公平锁 多个线程在申请锁的时候,按照申请的时间顺序依次获得锁
锁绑定多个条件 可以同时绑定多个Condition对象(可以绑定多个条件)
相比: synchronized只能绑定一个条件)

方法三: 使用第三方同步互斥锁(适用于分布式系统场景下)

可以使用Zookeeper、Redission分布式锁, 实现在分布式系统下的资源同步。

3.2 非阻塞同步

原理: 先进行正常操作,如果发现有线程操作冲突,则再进行处理。

可以通过 CPU的CAS指令(Compare-and-swap)实现(JDK1.5之后)。

四、锁优化

使用同步互斥锁,会阻塞等待中的线程(使其挂起),而挂起线程、恢复线程都算是重量级操作(这些操作需要转入内核进行),给操作系统的并发与性能带来不小的压力。

JDK1.6后,引入了一系列的锁优化技术,尽量减少线程直接的挂起,主要如下:

4.1 自旋锁 & 自适应自旋锁

自旋锁
qq20170930-092844 2x

可以从上图中看到:自旋锁,将原先使得线程挂起的操作 改为自循环,等到锁资源释放后,再继续。这种操作节省了线程挂起/恢复的开销,但是占用了处理器处理的时间。

JDK1.6后默认开启锁的自旋,当然,锁自旋是有限制的,如果超过自定次数的自旋后还没获得锁,就直接挂起线程(用 -XX: PreBlockPin 参数来配置自旋次数,默认10)

自适应自旋锁

在自旋锁基础上,自旋的时间不固定,而是由前一次同一个锁的自旋状态以及时间决定。

4.2 锁消除

JVM的JIT编译器在编译的时候,对于一些代码写着要同步锁,但是实际不存在数据资源竞争,JVM会消除这种锁。

4.3 锁粗化(扩大锁的范围)

正常情况下,我们总是尽量缩小锁的范围,但是对于一些频繁在同一个对象上加锁的操作,甚至在循环中没有竞争的情况下加锁,这个时候JVM会将锁的范围适当的扩大,节省加锁的次数。

4.4 轻量级锁

如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

4.5 偏向锁

在无竞争的情况下,把整个同步操作都消除了。

@johnnian johnnian changed the title Java基础-线程安全与锁 Java基础—线程安全与锁 Oct 2, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant