目录
- 引言
- 线程安全性
1.什么是线程安全性
2.一些相关概念
3.如何保证对象的线程安全性
4.哪些对象是需要使用同步机制的 - Java基本的同步方式
1.synchronized
2.显示锁(lock)
3.volatile
4.原子变量
一.引言
现代计算机程序,基本都运行在多核多线程的环境之中,如果不懂并发编程,那么我们编写出来的程序在多线程并发环境下很可能出现不可预知的错误,因此掌握并理解并发编程是成为一名优秀程序员的必备技能。本人将分享Java并发编程的一些知识及学习心得,主要基于对《Java并发编程实战》这本书的学习和本人平时的一些工作经验。
二.线程安全性
1.什么是线程安全性
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
2.一些相关概念
共享:意味着变量可以被多个线程同时访问。
可变:意味着变量的值可以在其生命周期内可以发生变化。
对象的状态:可以理解为对象的属性或一组属性。
原子操作:一组操作,要么全部执行完,要么不执行。
复合操作:一组操作,必须保证执行的原子性,才能确保线程安全。
正确性:某个类的行为与其规范完全一致,所见即所知。
竞态条件:由于不恰当的执行时序而出现不正确的结果。
正是由于许多看似原子操作的操作,实际上是复合操作,而又没有加上正确的同步,才会出现竞态条件,也就是多线程并发访问产生的不正确性。例如,i++自增操作,实际上是由“读取-修改-写入”这三个操作组成的复合操作,所以在多线程并发的条件下会产生不正确的结果。
3.如何保证对象的线程安全性
(1)无状态的对象一定是线程安全的
例如没有任何属性的对象。
public class Test {
public int add(int count) {
return count++;
}
}
(2)不在线程之间共享状态变量
例如该对象中属性只有单线程可以访问。
(3)对象状态变量均为不可变的
例如我们经常写的一些常量类,里面的属性均为final修饰的基本类型。
public class Test {
public static final int count = 10;
}
(4)访问对象的状态变量时使用正确的同步
通过使用正确的同步机制访问对象的状态变量,java提供了synchronized、显示锁、volatile、原子变量等同步机制。一个类的状态可能由多个变量组成,那么访问这些变量的时候要使用同一个锁。例如下面的test类,该类的状态由count和hit两个变量决定,如果在修改两个变量的时候不使用同一个锁,如下所示,就会出现错误。
package com;
//错误示例
public class Test {
private int count;
private int hit;
public int addHit() {
//使用Test类对象作为锁
synchronized (Test.class) {
hit++;
return hit;
}
}
public int addCount() {
//使用当前对象作为锁
synchronized (this) {
if(hit % 2 == 0) {
count++;
return count;
} else {
return -1;
}
}
}
public static void main(String[] args) {
Test test = new Test();
new Thread() {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i <= 100000; i++) {
int hit = test.addHit();
int count = test.addCount();
System.out.println("线程A, hit:" + hit +" count:" + count);
}
}
}.start();
new Thread() {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 01; i < 100000; i++) {
int hit = test.addHit();
int count = test.addCount();
System.out.println("线程B,hit:" + hit +" count:" + count);
}
}
}.start();
}
}
运行截图
正确写法:
public class Test {
private int count;
private int hit;
public int addHit() {
//线程安全,使用当前对象作为锁
synchronized (this) {
hit++;
return hit;
}
}
public int addCount() {
//线程安全,使用当前对象作为锁
synchronized (this) {
if(hit % 2 == 0) {
count++;
return count;
} else {
return -1;
}
}
}
}
正确结果截图:
4.哪些对象是需要使用同步机制的
对象的状态变量是共享和可变的时候,需要使用同步机制对其进行访问。
三.Java基本同步方式
1.synchronized
Java提供隐式锁、可重入锁,其用法:
(1)修饰一段代码块
任意java对象皆可作为锁,叫做内置锁,又叫做监视器锁,内置锁是可重入的。
public class Test {
public int count;
public int add(int count) {
//线程安全,使用当前对象作为锁
synchronized (this) {
return count++;
}
}
}
(2)修饰实例方法
就是使用当前对象作为锁。
public class Test {
public int count;
public synchronized int add(int count) {
//线程安全,使用当前对象作为锁
return count++;
}
}
(3)修饰静态方法
就是使用当前对象的类对象~~~~
public class Test {
public class Test1 {
private static int count;
/**
* 使用Test.class对象作为锁
*/
public synchronized static void add() {
count++;
}
}
2.volatile
(1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(可见性)
(2)禁止进行指令重排序。(有序性)
(3)volatile 只能保证对单次读/写的原子性。
上面这几条是我在网上找到的对volatile特性的概述,具体详情本文就不详述了。但是何时使用volatile,只要记住一句话:当你能确保只有一个线程写变量的时候,这个变量就可以用volatile修饰,多少个线程读没有关系。volatile比直接加锁更加轻量级,所以根据场景来使用加锁还是volatile。
其实在平时工作中,我们只要能理解并发的概念和场景,然后能正确使用synchronized和volatile,就能应付大部分的并发场景了。
3.显示锁
就是使用Java的Lock与ReentrantLock,具体本文暂不详述。
4.原子变量
使用Java的AtomicInteger等变量,顾名思义这些变量的方法都是原子性的、线程安全的,具体本文暂不详述。