java中为了多线程安全问题,我们往往会要求线程同步操作,以下是java中常用的几种实现线程同步的方式。
1、使用synchronized关键字修饰方法
class ThreadA{
int count = 0;
synchronized public void count()
{
System.out.println(count);
count++;
}
}
public class Test {
public static void main(String[] args) {
ThreadA a = new ThreadA();
for (int i = 0; i < 5; i++) {
Thread test = new Thread(){
@Override
public void run() {
a.count();
}
};
test.start();
}
}
}
运行结果:
0
1
2
3
4
给count()加了关键字synchronized保证同一时刻每次只有一个线程访问方法
2、给代码块加上synchronized关键字
class ThreadA{
int count = 0;
public void count()
{
System.out.println(Thread.currentThread().getName() + "第一次进入方法时count值:" + count);
synchronized(this)
{
count++;
System.out.println(Thread.currentThread().getName() + "自增改变后count值:" + count);
}
}
}
public class Test {
public static void main(String[] args) {
ThreadA a = new ThreadA();
for (int i = 0; i < 5; i++) {
Thread test = new Thread(){
@Override
public void run() {
a.count();
}
};
test.start();
}
}
}
给代码块加synchronized关键字时,需要把代表当前对象this传入
3、使用volatile关键字
原理:每次要线程要访问volatile修饰 的变量时都是从主内存中读取,而不是从缓存当中读取,即保证了线程每次读取的变量都是最新的,因此每个线程访问到的变量值都是一样的。这样就保证了同步。但是volatile虽然保证了多线程的可见性,但是并没有保证操作的原子性,如果有读和写操作还是会导致线程不安全。
class ThreadA{
volatile int count = 0;
public void count()
{
count++;
}
}
public class Test {
public static void main(String[] args) {
ThreadA a = new ThreadA();
for (int i = 0; i < 5; i++) {
Thread test = new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "方法前值为:" + a.count);
a.count();
System.out.println(Thread.currentThread().getName() + "方法后值为:" + a.count);
}
};
test.start();
}
}
}
运行结果:
Thread-0方法前值为:0
Thread-2方法前值为:0
Thread-3方法前值为:0
Thread-1方法前值为:0
Thread-3方法后值为:3
Thread-4方法前值为:4
Thread-2方法后值为:2
Thread-0方法后值为:1
Thread-4方法后值为:5
Thread-1方法后值为:4
说明:
volatile关键字比较适合set或者get场景,对于有get和operate场景是不适合的,因为一般的操作其实本质上都包含着读取-修改-写入这一系列步骤,但是volatile是不保证操作的原子性的,所以这时会有线程安全问题。
线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。
延伸扩展:
每一个线程都有一个线程栈,线程栈中会保存一些访问对象的成员变量的值的信息。当线程访问一个对象的成员变量时,线程会从主内存(对象引用指向的堆内存)中读取这个变量,复制一份存储到自己线程的线程栈中,线程操作的时候,操作的是线程栈中的数据,操作完之后,把这个数据同步回写到主内存中。
read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容
其中use and assign 可以多次出现,但是这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如:
假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值
在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6
线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6
导致两个线程及时用volatile关键字修改之后,还是会存在线程安全的问题。
4、使用重入锁
reentrantLock和synchronized具有相同的基本行为和语义,但是又扩展了其能力
代码:
class ThreadA{
int count = 0;
private Lock lock = new ReentrantLock();
public void count()
{
lock.lock();
System.out.println(Thread.currentThread().getName() + "方法前值为:" + count);
try {
count++;
System.out.println(Thread.currentThread().getName() + "方法后值为:" + count);
} finally {
lock.unlock();
}
}
}
public class Test {
public static void main(String[] args) {
ThreadA a = new ThreadA();
for (int i = 0; i < 5; i++) {
Thread test = new Thread(){
@Override
public void run() {
a.count();
}
};
test.start();
}
}
}
注意:使用重入锁lock()之后一定要记得unlock(),一般把unlock()放在finally代码块中,不然会导致线程死锁。
关于更多重入锁的使用方法,可以参考这位大神的博客:
https://www.cnblogs.com/-new/p/7256297.html