线程同步的方法
1、使用synchronized获取对象互斥锁:这个最常用的的也是比较安全的一种方式,采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。
public class Test
public static void main(String[] args) throws IOException {
Test test = new Test();
Test.MyThread thread1 = test.new MyThread();
Test.MyThread thread2 = test.new MyThread();
thread1.start();
thread2.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
synchronized (object) {
i++;
System.out.println("i:"+i);
try {
System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");
Thread.currentThread().sleep(10000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");
i++;
System.out.println("i:"+i);
}
}
}
2、使用特殊域变量volatile实现线程同步:volatile修饰的变量是一种稍弱的同步机制,因为每个线程中的成员变量都会对这个对象私有拷贝,每个线程获取的数据都是从私有拷贝中获取到的。而当volatile修饰之后,代表这个对象只能从共享内存中获取,禁止私有拷贝。访问volatile变量时不会执行加锁操作,因此也不会执行线程阻塞。因此volatile变量是一种比synchronize关键字更轻量级的同步机制。但是因此会比使用加锁更加不安全,使用同步机制会更安全一些,当且仅当满足以下所有条件时,才应该使用volatile变量,1、对变量的写入操作不依赖当前值,或者能确保只有单个线程更新变量的值。该变量没有包含在具有其他变量的不变式中。
class Bank {
//需要同步的变量加上volatile
private volatile int account = 100;
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
account += money;
}
}
3、使用重入锁Lock实现线程同步,这是在jdk5以后才导入的同步方式,因为synchronize同步之后会存在一个阻塞的过程,如果这个阻塞的时间过久,验证影响我们代码的质量以及系统系能上的问题,我们需要一种机制,让等待的线程到达一段时间以后能响应中断,这就是lock的作用。
在使用synchronize关键字实现同步的话,在遇到IO阻塞的情况时,其他线程就只能等待无法进行操作,这样十分影响效率。而Lock可以让等待的线程进入中断先去执行其他任务。
对于中断,Lock中的两种锁有不同的响应方式,lock方法会优先考虑获取锁,等到锁获取成功后才响应中断,而lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。
static Lock lock=new ReentrantLock();
private static void do321() throws InterruptedException {
lock.lock();
System.out.println(Thread.currentThread().getName() + " is locked.");
try {
System.out.println(Thread.currentThread().getName() + " doSoming1....");
Thread.sleep(10000);// 此时才会响应中断
System.out.println(Thread.currentThread().getName() + " doSoming2....");
System.out.println(Thread.currentThread().getName() + " is finished.");
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("Thread 1 get lock");
do321();
System.out.println("Thread 1 end");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("thread 1 is interrupted!!!");
}
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("Thread 2 get lock");
do321();
System.out.println("Thread 2 end");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("thread 2 is interrupted!!!");
}
}
});
thread1.setName("Thread 1");
thread2.setName("Thread 2");
thread1.start();
Thread.sleep(1000);
thread2.start();
Thread.sleep(1000);
thread2.interrupt();
}
output:
Thread 1 get lock
Thread 1 is locked.
Thread 1 doSoming1....
Thread 2 get lock
Thread 1 doSoming2....
Thread 1 is finished.
Thread 1 end
Thread 2 is locked.
Thread 2 doSoming1....
thread 2 is interrupted!!!
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.lock.test.Test.do321(Test.java:26)
at com.lock.test.Test$2.run(Test.java:56)
at java.base/java.lang.Thread.run(Thread.java:830)
从例子中可以看出使用了lock锁,当出现中断的时候Thread2线程不会马上响应中断,而是等Thread1释放锁,Thread2获取锁以后才响应的中断
static Lock lock=new ReentrantLock();
private static void do123() throws InterruptedException {
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " is locked.");
try {
System.out.println(Thread.currentThread().getName() + " doSoming1....");
Thread.sleep(10000);// 等待几秒方便查看线程的先后顺序
System.out.println(Thread.currentThread().getName() + " doSoming2....");
System.out.println(Thread.currentThread().getName() + " is finished.");
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("Thread 1 get lock");
do123();
System.out.println("Thread 1 end");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("thread 1 is interrupted!!!");
}
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("Thread 2 get lock");
do123();
System.out.println("Thread 2 end");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("thread 2 is interrupted!!!");
}
}
});
thread1.setName("Thread 1");
thread2.setName("Thread 2");
thread1.start();
Thread.sleep(1000);
thread2.start();
Thread.sleep(1000);
thread2.interrupt();
}
output:
Thread 1 get lock
Thread 1 is locked.
Thread 1 doSoming1....
Thread 2 get lock
java.lang.InterruptedException
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:944)
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1263)
at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:317)
at com.lock.test.Test.do123(Test.java:9)
at com.lock.test.Test$2.run(Test.java:56)
at java.base/java.lang.Thread.run(Thread.java:830)
thread 2 is interrupted!!
Thread 1 doSoming2....
Thread 1 is finished.
Thread 1 end
从这个例子中可以看出lockInterruptibly锁,会立即响应中断,而是等获取锁以后在响应。
4、使用ThradLocal管理变量:如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal() : 创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的"初始值"
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
public class MyThreadLocal {
private static ThreadLocal<Object> threadLocal=new ThreadLocal<Object>(){
@Override
protected Object initialValue() {
System.out.println("调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!");
return 5;
}
};
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
int nums= (int) threadLocal.get();
while(nums>0) {
System.out.println("书a总数为:" + nums);
nums--;
threadLocal.set(nums);
}
nums=(int) threadLocal.get();
System.out.println("书a剩余数量:"+nums);
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
int nums= (int) threadLocal.get();
while(nums>0) {
System.out.println("书b总数为:" + nums);
nums--;
threadLocal.set(nums);
}
nums=(int) threadLocal.get();
System.out.println("书b剩余数量:"+nums);
}
});
thread1.start();
Thread.sleep(100);
thread2.start();
}
}
output:
调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!
书a总数为:5
书a总数为:4
书a总数为:3
书a总数为:2
书a总数为:1
书a剩余数量:0
调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!
书b总数为:5
书b总数为:4
书b总数为:3
书b总数为:2
书b总数为:1
书b剩余数量:0
从例子中我们可以看出,虽然Thread1已经初始化了ThreadLocal里的值,但是当Thread2去获取的时候,任然重新初始化了,可以看出不同线程都会获取只属于自己的ThreadLocal。
ThreadLocal在对于那些线程不安全的工具类的使用中有很大的用处:例如SimpleDateFomat是线程不安全的,但是若每次使用都重新new的话又很繁琐,但是若使用ThreadLocal封装这个类就完全可以避免这个问题,阿里巴巴java开发手册是这么推荐的:
public class DateUtils {
public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
}
当不同线程要使用SimpleDateFormat的时候,根据ThreadLocal的机制我们可以得知,第一次使用get方法的时候都会调用initialValue()方法,这样不同线程获取的就是不同的SimpleDateFormat对象,这样也就避免了线程不安全的问题了。