java基础(jdk1.8)
**
多线程—volatile、原子类及synchronized使用
**
此篇博客主要讲volatile及synchronized关键字的使用
情景
我们先要知道这么一个概念,那就是什么是线程安全。简单来说,线程安全就是在多线程状态下程序执行结果和单线程执行结果相同。先看这个代码块
public class A{
int count = 0;
public static void main(String []args)
{
A a = new A();
for (int i = 0 ; i < 10 ; i++)
{
for (int j = 0 ; j < 1000 ; j++){
a.count++;
}
}
System.out.println(a.count);
}
}
/*
这个小代码块,则是10个对a进行1000次++操作
结果自然是10000
*/
接下来使用多线程试试
public class A{
int count = 0;
public static void main(String []args)
{
A a = new A();
for (int i = 0 ; i < 10 ; i++)
{
new Thread(()->{
for (int j = 0 ; j < 1000 ; j++){
a.count++;
}
}).start();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.count);
}
}
/*
这个小代码块,则是10个线程对a进行1000次++操作
结果则加不到10000
这就是线程的安全性问题
*/
这里就有一个问题则是这个++操作,因为其分为三步,
第一步:取值
第二步:+1
第三步:赋值
而且,取值与赋值是相对于缓存操作的,具体看volatile
volatile关键字
- 功能
保证内存可见性&&禁止指令重排序
-
使一个变量在多个线程中可见
-
一个线程在更改变量后,会通知其他线程变量改变,使其他线程更改内容:
高并发过程中,线程对一个变量的访问可能在缓存中,而主存的内容改变,没有 volatile关键字的情况下,线程一直访问缓存,导致程序不能及时刷新而出现问题。加了volatile之后,变量改变后,会通知所有线程,使其各自的缓冲更新一次,达到精准的效果。
- 但volatile不能保证多个线程对变量共同修改导致的不一致问题,只有可见性,而 synchronized既有原子性也保证可见性,但效率相对低一些。
- 使用方式
class A{
int a;
//volatile关键字直接加到字段上即可保证这个字段有上面两个功能
volatile int b;
}
- 注意
这个volatile并不能保证上面的代码可以加到10000,但是可以保证读取数据时的一个读取正常
相对来说,加上volatile比不加会稍微高一些,当还是有问题
synchronized关键字
- 功能
锁,互斥锁
任何线程想执行以下代码的时候,必须先申请(堆上对象,而不是引用)的锁
synchronized是互斥锁(只要有人拿到锁,其他就拿不到锁)
-
从程序角度考虑,synchronized相当于一个原子性操作,此代码块为一个原子操作,不可被打断
-
synchronized是对对象加锁,同一个对象执行到synchronized时,必须获取对象锁才能继续执行
-
在一个线程访问synchronized的过程中,其他线程是可以访问该对象的非synchronized方法(读写问题,写加锁,读不加锁,产生脏读现象 )
-
在一个线程获取到对象锁的时候,再次申请该对象的锁时还是可以获取到该对象的锁,所以synchronized获得的锁是可重入的
-
子类同步方法可以调用父类同步方法
-
synchronized 锁遇到异常时会释放,并发编程中必须对异常处理。
-
synchronized 加在static方法上,锁的是(类.class),非静态方法,则锁的是(this)
- 使用
public class A{
int count = 0;
public static void main(String []args)
{
A a = new A();
for (int i = 0 ; i < 10 ; i++)
{
new Thread(()->{
synchronized (a)
{
for (int j = 0 ; j < 1000 ; j++){
a.count++;
}
}
}).start();
}
//使用睡眠保证在所有的线程执行完然后主线程输出count的值
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.count);
}
}
synchronized使用两种方式:
1.直接加在方法上
2.使用以上代码块的方式
3.其他位置不行
- 注意
加到普通方法即和代码块上(this)相同,加到静态方法和代码块(A.class)
原子操作类
AtomicInteger类的incrementAndGet()方法代替++,(不用synchronized)可以使效率更高。AtomicXXX的几个原子性方法并用,则破坏其原子性,也会被其他线程插入
原子操作类在操作时使用了CAS
上代码
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class A{
//volatile只保证可见性,所以并不能使count到100000
/*volatile*/ int count = 0;
//原子性操作的数据,效率比synchronized和count++更高
AtomicInteger acount = new AtomicInteger(0);
/*synchronized*/ void m(){
for (int i = 0;i<10000;i++)
{
count++;
acount.incrementAndGet();//原子性的 count++
}
}
public static void main(String[] args) {
List list = new ArrayList();
list.add("");
A v = new A();
List <Thread> threads = new ArrayList<>();
for (int i = 0;i<10;i++)
{
threads.add(new Thread(v::m,"Thread:"+i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try{
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(v.count);
System.out.println(v.acount);
}
}
常见问题
-
synchronized代码优化,代码块中代码尽量少写
-
synchronized锁定的是new 堆的对象,而不是引用内存,如果对象的引用改变,则也会改变
:Object o = new Object();
o = new Object();//此时的锁也会相应改变。 -
synchronized不要将String作为锁定对象,因为字符串内容一样时,其锁是同一个字符串常量
-
高并发过程中
(1)在并发量较大的情况下increment 自增
(2)synchronized加锁和原子操作类相比,原子操作类还是速度快一些
(3)LongAdder 在并发量相当高的时候效率会非常快
使用数组将其分开,平均线程去操作数组的每个值,最后合成
上一篇 》锁的类型及概念
下一篇 》ReentrantLock使用