线程互斥可以理解为多线程可能同时对某一资源进行操作,造成紊乱。因此需要给这一资源进行加锁,比如一个马桶,一个人在用时,其他人不能也不该去抢着用,这时就需要进行加锁操作。
加锁一般有两种方式,synchronized和显示的lock。
1. synchronized
synchronized可以用于同步方法或者是同步代码块,用于同步方法时,加锁对象就是方法所属的对象,同步代码块时需要显式指出加锁对象。
如:
synchronized(this){}
就是指明当前对象为加锁对象。
需要注意的是synchronized加锁的目标是对象实例,而不是方法,更不是引用,一个任务可以获得多次锁,一个对象的同步方法(块)A中调用对象的另一个同步方法(块)B,此时某任务运行了方法A,就会获得两次锁(synchronized是可重入锁),只有在锁被完全释放,其他任务才可以使用此对象。
public synchronized void oneMethod(){
a++;
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}
twoMethod();
a++;
}
public synchronized void twoMethod(){
a--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
线程互斥的根本在于共享同一资源,所以如果你创建了多个对象,还谈什么同步,错误示例如下:
public class Wrong {
private List<String> strings = new ArrayList<>();
public synchronized void method1(){
strings.add("aaa");
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
new Wrong().method1();
}
});
new Thread(new Runnable() {
@Override
public void run() {
new Wrong().method1();
}
});
}
}
显然毫无关联,两个线程间不会有什么共享资源的问题,因为压根就是两个对象。
静态方法
静态方法前也可以加上synchronized修饰符,用来在类的范围内防止对static数据的并发访问,此时的加锁对象是该类的Class对象。实例中的put方法和print方法的加锁是等价的。
public class StaticSynchronized {
private static List<Integer> list = new ArrayList<>();
public synchronized static void put(){
for (int i = 0;i < 5; i++){
list.add(i);
}
}
public static void print(){
synchronized (StaticSynchronized.class){
for (int i:list){
System.out.print(i + " ");
}
System.out.println();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 4; i++){
exec.execute(new Runnable() {
@Override
public void run() {
StaticSynchronized.put();
}
});
}
exec.shutdown();
Thread.sleep(1000);
StaticSynchronized.print();
}
}
2 lock
lock是一个接口,唯一实现类为ReentLock。它需要显式的创建锁这个对象,也需要显式的释放锁,锁的对象就是lock本身。由于他的释放锁是显式的,常常放在finnally中,并且在finnally中做些其他的清理工作(关闭io流等),维持系统稳定。而synchronized中,如果某些事情失败,会直接抛出异常,没有时间清理系统。
Lock主要有三种创建锁的方式:lock,tryLock,lockInterrputibly
返回值 | 方法名(参数) |
---|---|
void |
lock() 获取锁。 |
void |
lockInterruptibly() 如果当前线程未被中断,则获取锁。 |
Condition |
newCondition() 返回绑定到此 Lock 实例的新 Condition 实例。 |
boolean |
tryLock() 仅在调用时锁为空闲状态才获取该锁。 |
boolean |
tryLock(long time, TimeUnit unit) 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。 |
void |
unlock() 释放锁。 |
1. 使用lock
private int value = 0;
private Lock lock = new ReentrantLock();
@Override
public int next() {
lock.lock();
try {
value++;
value++;
return value;
}finally {
lock.unlock();
}
//return value;
}
finnally中释放锁,在lock与unclok之间的代码块被加锁。lock需要显式的创建,常常是创建为成员变量,如果在方法体内创建锁实例,就毫无加锁的效果。这是因为lock的锁是客观存在的。如果创建了多个锁实例,那么多个锁实例间,加锁解锁并无关联.错误示例:
public class WrongLock {
private ArrayList<Integer> arrayList = new ArrayList<>();
public static void main(String[] args){
WrongLock wrongLock = new WrongLock();
new Thread(new Runnable() {
@Override
public void run() {
wrongLock.insert(Thread.currentThread());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wrongLock.insert(Thread.currentThread());
}
}).start();
}
public void insert(Thread thread){
Lock lock = new ReentrantLock();
lock.lock();
try {
System.out.println(thread.getName()+ " 得到了锁");
for (int i = 0; i< 5; i++){
arrayList.add(i);
}
}finally {
System.out.println(thread.getName() + "释放了锁");
lock.unlock();
}
}
}
最终结果为
Thread-0得到了锁
Thread-1得到了锁
Thread-0释放了锁
Thread-1释放了锁
这个错误与上面的synchronized的那个错误示例本质一样。
2. tryLock
tryLock意思是尝试获取锁,如果已经被占用,则返回false,如果未被占用就获取锁。这样可以灵活的利用阻塞时间,如果已经被加锁了就可以去执行其他任务。这是synchronized做不到的。
public class TryLockTest {
private Lock lock = new ReentrantLock();
private void method(){
if (lock.tryLock()){
try {
System.out.println(Thread.currentThread().getName() + " try and get lock, start to do work A");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finished work A");
}finally {
lock.unlock();
}
}else {
System.out.println(Thread.currentThread().getName()+ " try but not get lock, start to do work B");
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finished work B");
}
}
public static void main(String[] args) {
TryLockTest tryLockTest = new TryLockTest();
new Thread(){
@Override
public void run() {
tryLockTest.method();
}
}.start();
new Thread(){
@Override
public void run() {
tryLockTest.method();
}
}.start();
}
}
最终结果:
Thread-0 try and get lock, start to do work A
Thread-1 try but not get lock, start to do work B
Thread-1 finished work B
Thread-0 finished work A
需要注意的是,只有if(true)时,才会加锁,最后需要在finnally中释放锁。false时不会加锁。
3. lockInterruptibly
如果获取了锁立即返回,如果没有获取锁,当前线程处于休眠状态,直到锁定,或者当前线程被别的线程中断。
public class lockInterruptiblyTest {
private Lock lock = new ReentrantLock();
public void test(){
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 获取到锁");
Thread.sleep(1000 * 5);
}catch (InterruptedException e){
System.out.println("睡眠中断");
}finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "等待获取锁时被中断");
}
}
public static void main(String[] args) throws InterruptedException {
lockInterruptiblyTest test = new lockInterruptiblyTest();
Thread thread1 = new Thread(){
@Override
public void run() {
test.test();
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
test.test();
}
};
thread1.start();
thread2.start();
Thread.sleep(2*1000);
thread2.interrupt();
}
}
thread2在等待获取锁时(休眠时)被打断。最终结果:
Thread-0 获取到锁
Thread-1等待获取锁时被中断
3. synchronized和lock的区别
1. 性能区别
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码,在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍;但是ReetrantLock的性能能维持常态;
2. 用途区别
synchronized和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,考虑使用ReentrantLock,特别是遇到下面几种需求的时候。
A. 某个线程在等待一个锁的控制权的这段时间需要中断
B. 需要尝试获取锁,获取不到要去做其他事情时。
C. 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
3. 使用区别
synchronized:
在需要同步的对象中加入此控制,synchronized可以加在方法上,也加在特定代码块中,括号中表示需要锁的对象
Lock:
需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效,锁的对象就是Lock本身。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁
4. 原子性与可视性
1. atomicity
如果一个操作不可分割,那么说他具有原子性
原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”,比如读取return,赋值=,可以保证他们确实是原子操作。
但是long和double类型是64位的,其读取写入会被jvm当做分离的32位操作,从而导致不同的任务看到不正确结果的可能性(字撕裂)。如果为long,double使用volatile,就会获得读写的原子性。
值得注意的是,自增(减)操作在java中绝对不是原子性操作。而且不能依赖原子性来保证线程安全。
public class AtomicityTest implements Runnable {
private volatile int i = 0;
public int getValue() { return i; }
private synchronized void evenIncrement() { i++; i++; }
public void run() {
while(true) {
evenIncrement();
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
AtomicityTest at = new AtomicityTest();
exec.execute(at);
while(true) {
int val = at.getValue();
if(val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}
getValue方法依靠了原子性,但是其不是同步方法,它可能在evenIncrement方法使用一半时getValue。因此其线程并不安全
2. volatile
可视性问题是指,多线程访问同一内存时,会创建各自私有备份(缓存),来优化jvm,缓存并不会立即同步到内存,因此其他线程看到的并不一定是真实的数据。
如果在变量上使用volatile关键字,则会告诉jvm,这个变量不会被线程缓存,而是直接对内存修改。若已经为同步方法(代码块),则必然是可视的。
3. 原子类
AtomicInteger,AtomicLong,AtomicReference等特殊的原子性变量类,使用这些类时,不需要再使用同步
public class AtomicIntegerTest implements Runnable {
private AtomicInteger integer = new AtomicInteger();
public int getValue(){
return integer.get();
}
private void evenIncrement(){
integer.addAndGet(2);
}
@Override
public void run() {
while (true){
evenIncrement();
}
}
public static void main(String[] args) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
System.out.println("over");
System.exit(0);
}
}, 5000);
ExecutorService exec = Executors.newCachedThreadPool();
AtomicIntegerTest ait = new AtomicIntegerTest();
exec.execute(ait);
while (true){
int val = ait.getValue();
if (val % 2 !=0){
System.out.println(val);
System.exit(0);
}
}
}
}
此时getValue和evenIncrement都不是同步方法,但是保证了其安全性。
5.ThreadLocal
防止共享资源上产生冲突,ThreadLocal提供另一种解决思路,根除对变量的共享,ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
这是一种以空间换时间的策略,但是这也是一种“为了防止吃饭噎着,就不吃饭”的策略,其并不解决共享变量的问题,只是回避,如果需要多线程操作同一变量,这种方法明显不可取。
其在原理上实际是通过Map,键是 Thread,值是它在该 Thread 内的实例。线程通过该 ThreadLocal 的 get() 方案获取实例时,只需要以线程为键,从 Map 中找出对应的实例即可。
返回值 | 方法名 |
---|---|
T |
get() 返回此线程局部变量的当前线程副本中的值。 |
protected T |
initialValue() 返回此线程局部变量的当前线程的“初始值”。 |
void |
remove() 移除此线程局部变量当前线程的值。 |
void |
set( Tvalue) 将此线程局部变量的当前线程副本中的值设置为指定值。 |
initialValue:用于为线程副本初始化。线程第一次使用 get() 方法访问变量时将调用此方法,但如果线程之前调用了 set(T) 方法,则不会对该线程再调用 initialValue 方法。
ThreadLocal在使用时一般作为静态成员。
错误示例:
public class ThreadLocalTask implements Runnable{
private static ThreadLocal<Pair> pairThreadLocal = new ThreadLocal<Pair>();
public ThreadLocalTask(Pair pair){
pairThreadLocal.set(pair);
}
@Override
public void run() {
Pair pair = pairThreadLocal.get();
pair.incrementY();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
pair.incrementX();
pair.checkState();
System.out.println(Thread.currentThread().getName() + pair);
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
Pair pair = new Pair();
for (int i = 0; i < 5; i++){
exec.execute(new ThreadLocalTask(pair));
}
//System.out.println(pairThreadLocal.get());
exec.shutdown();
}
}
此示例企图通过Task的构造方法来set,但是set方法是为当前线程指定副本,而在new ThreadLocalTask时线程还是main线程,因此一直只是为main线程使用set,其他线程在Run()方法中使用get,都是得到的null(因为并没有复写initialValue)
正确示例:
public class ThreadLocalTask implements Runnable{
private static ThreadLocal<Pair> pairThreadLocal = new ThreadLocal<Pair>(){
@Override
protected Pair initialValue() {
return new Pair();
}
};
@Override
public void run() {
Pair pair = pairThreadLocal.get();
pair.incrementY();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
pair.incrementX();
pair.checkState();
System.out.println(Thread.currentThread().getName() + pair);
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++){
exec.execute(new ThreadLocalTask());
}
//System.out.println(pairThreadLocal.get());
exec.shutdown();
}
}
此时复写了initialValue方法,这样在出此调用get时就会为当前线程创建实例,但是可以发现,其实这很蠢,已经没有任何共享资源可言了。
网友评论