背景
在上篇文章java多线程:volatile的深度理解文末,提到了volatile与synchronized的主要区别:
- volatile关键字解决的是内存可见性的问题
- synchronized关键字解决的是执行控制的问题
该篇文章继续对java多线程:synchronized的应用进行解析。
顺序一致型内存模型
所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见
可以用以下示意图来理解这个模型:
内存通过一个左右摆动的开关可以连接到任意一个线程,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化。
synchronized的原理
查看以下代码:
/**
* synchronized内存一致性模型
*
* @author zhuhuix
* @date 2020-05-07
*/
public class SynchronizedThread implements Runnable {
int v = 0;
public synchronized void Increment() {
v++;
System.out.println("线程:" + Thread.currentThread().getName() + "获取的静态值为:" + v);
}
@Override
public void run() {
Increment();
}
public static void main(String[] args) {
SynchronizedThread synchronizedThread = new SynchronizedThread();
for (int i = 0; i < 50; i++) {
new Thread(synchronizedThread).start();
}
}
}
synchronized 的加锁操作,实际上变成了串行程序:
等同于以下代码:而且多线程程序频繁的加锁释放锁,导致执行效率还不如单线程
/**
* 单线程运行
*
* @author zhuhuix
* @date 2020-05-07
*/
public class Serial {
int v = 0;
public void Increment() {
v++;
System.out.println("获取的静态值为:" + v);
}
public static void main(String[] args) {
Serial serial = new Serial();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
Long start =System.currentTimeMillis();
System.out.println(sdf.format(start));
for (int i = 0; i < 50; i++) {
serial.Increment();
}
Long end = System.currentTimeMillis();
System.out.println(sdf.format(end));
System.out.println(end-start);
}
}
synchronized的实例解析
首先我们来看一段单线程代码:该段程序实现了对四个随机打乱的list做了冒泡排序,在排序过程中,对静态变量k和v进行了相加计算。
代码段1
/**
* 单线程冒泡排序
*
* @author zhuhuix
* @date 2020-05-07
*/
public class SerialBubble {
public static Long k=0L;
public static Long v=0L;
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
List<Integer> list3 = new ArrayList<>();
List<Integer> list4 = new ArrayList<>();
for (int i = 1; i < 10000; i++) {
list1.add(i);
list2.add(i);
list3.add(i);
list4.add(i);
}
//随机打乱list1
Collections.shuffle(list1);
//随机打乱list2
Collections.shuffle(list2);
//随机打乱list3
Collections.shuffle(list3);
//随机打乱list4
Collections.shuffle(list4);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
Long start = System.currentTimeMillis();
System.out.println("单线程程序开始时间:" + sdf.format(start));
List<Integer> list1Sort = bubbleSort((list1));
List<Integer> list2Sort = bubbleSort((list2));
List<Integer> list3Sort = bubbleSort((list3));
List<Integer> list4Sort = bubbleSort((list4));
Long end = System.currentTimeMillis();
System.out.println("单线程程序结束时间:" + sdf.format(end));
System.out.println("排序耗用了" + (end - start) + "毫秒" + ";最终v的值=" + v);
}
public static List<Integer> bubbleSort(List<Integer> list) {
for (int i = 0; i < list.size() - 1; i++) {
for (int j = i + 1; j < list.size(); j++) {
if (list.get(i) > list.get(j)) {
int temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
}
add();
}
return list;
}
public static void add() {
for (int i = 0; i < 100; i++) {
k=k+i;
v=v+k;
}
}
}
运行结果:由于是单线程程序,k和v的值不存在线程共享与同步的问题,是安全的。
代码段2
为了加快程序的运行效果,对时间复杂度较高的冒泡算法,希望通过多线程运行,但运行过程中由于存在静态变量k和v的相加计算,必须要通过给对象的方法加上锁,防止线程不同步。
/**
* 多线程冒泡排序
*
* @author zhuhuix
* @date 2020-05-07
*/
public class ThreadBubble implements Runnable {
public static Long k=0L;
public static Long v=0L;
private List<Integer> list;
ThreadBubble(List<Integer> list) {
this.list = list;
}
public List<Integer> getList() {
return list;
}
/**
* 通过synchronized static 给静态变量上锁
*/
public static synchronized void add() {
for (int i = 0; i < 100; i++) {
k=k+i;
v=v+k;
}
}
@Override
public void run() {
for (int i = 0; i < list.size() - 1; i++) {
for (int j = i + 1; j < list.size(); j++) {
if (list.get(i) > list.get(j)) {
int temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
}
add();
}
}
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
List<Integer> list3 = new ArrayList<>();
List<Integer> list4 = new ArrayList<>();
for (int i = 1; i < 10000; i++) {
list1.add(i);
list2.add(i);
list3.add(i);
list4.add(i);
}
//随机打乱list1
Collections.shuffle(list1);
//随机打乱list2
Collections.shuffle(list2);
//随机打乱list3
Collections.shuffle(list3);
//随机打乱list4
Collections.shuffle(list4);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
Long start = System.currentTimeMillis();
System.out.println("多线程程序开始时间:" + sdf.format(start));
ThreadBubble bubbleSort1 = new ThreadBubble(list1);
Thread thread1 = new Thread(bubbleSort1);
thread1.start();
ThreadBubble bubbleSort2 = new ThreadBubble(list2);
Thread thread2 = new Thread(bubbleSort2);
thread2.start();
ThreadBubble bubbleSort3 = new ThreadBubble(list3);
Thread thread3 = new Thread(bubbleSort3);
thread3.start();
ThreadBubble bubbleSort4 = new ThreadBubble(list4);
Thread thread4 = new Thread(bubbleSort4);
thread4.start();
//四个线程都结束了,主程序才结束
while (thread1.isAlive() || thread2.isAlive() || thread3.isAlive() || thread4.isAlive()) {
}
Long end = System.currentTimeMillis();
System.out.println("多线程程序结束时间:" + sdf.format(end));
System.out.println("排序耗用了" + (end - start) + "毫秒"+";v的最终值为"+v );
}
}
运行结果如下:多线程在加快运算速度的前提下,通过synchronized锁的修饰保证了共享变量的同步。
总结
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
注意:
- 每个对象都可以做为锁,但一个对象做为锁时,应该被多个线程共享,这样才显得有意义,在并发环境下,一个没有共享的对象作为锁是没有意义的。
- 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁。
- 如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法 。
- 但不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;