- Java 并发总结, synchronized详解
- 1 线程安全
- 2 互斥同步
- 3 synchronized特性
- 4 synchronized的三种应用方式
- 4.1 synchronized修饰实例方法
- 4.2 synchronized作用于静态方法
- 4.3 synchronized作用于代码块
- 5 synchronized底层实现原理(待补充)
- 6 Java三大变量的线程安全(待补充)
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果。那么这个对象就是线程安全的。
Java 语言中得各种操作共享数据可以分成五类,按安全程度由强到弱来排序:
1 不可变:不可变的对象一定是线程安全的,无论对象的方法实现还是方法的调用都不需要在采取措施。
如果共享数据是一个基本数据类型,那么只要在定义的时候使用final 关键字修饰就可以保证它不可变;如果共享数据是一个对象,需要对象自行保证其行为不会对其状态产生任何影响。
2 绝对线程安全:绝对线程安全是不管运行环境如何,调用者都不需要任何额外的同步措施。通常需要付出很大的甚至不切实际的代价。
3 相对线程安全:就是通常意义的线程安全,确保这个对象单独的操作是安全的。Java语言中,大部分声称线程安全的类都属于这种类型,如Vector,Hashtable。
4 线程兼容:指的是本身对象并不是线程安全的,但是可以通过调用端的正确使用同步手段来保证对象的安全使用。通常我们说一个类不是线程安全的,指的就是这种状态,如:ArrayList,HashMap等。
5 线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中并发使用。
2 互斥同步互斥和同步是一种最常见、最主要的实现并发正确性(相对线程安全)的保障手段。
同步:指的是多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用。
互斥:指的是实现同步的一种手段,临界区互斥量和信号量都是主要的互斥实现方式。
在java中,最基本的互斥同步手段就是synchronized关键字。synchronized可以保证线程竞争共享资源的正确性(多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用)。
3 synchronized特性原子性:确保线程互斥的访问同步代码。synchronized保证只有一个线程拿到锁,进入同步代码块操作共享资源,因此具有原子性。
可见性:保证共享变量的修改能够及时课件。执行synchronized时,会对应执行lock,unlock原子操作。lock操作,就会清空工作空间该变量的值;执行unlock操作之前,必须先把变量同步回主内存中。
有序性:synchronized内的代码和外部的代码禁止排序,至于内部的代码,则不会禁止排序,但是由于只有一个线程进入同步代码块,因此在同步代码块中相当于是单线程的,根据as-if-serial语义,即使代码块内部发生了重排序,也不会影响程序执行的结果。
悲观锁:synchronized是悲观锁,每次使用共享资源时都认为会和其他线程产生竞争,所以每次使用共享资源都会上锁。
独占锁:synchronized是独占锁,该锁一次只能被一个线程所持有,其他线程被阻塞。
非公平锁:synchronized是非公平锁,线程获取锁的顺序可以不按照线程的阻塞顺序,允许线程发出请求后立即尝试获取锁。
可重入锁:synchronized是可重入锁。持锁线程可以再次获取自己的内部的锁。
Tips:
1 悲观锁 or 乐观锁:是否一定要锁。
2 共享锁 or 独占锁(排他锁):是否可以有多个线程同时拿锁。
3 公平锁 or 非公平锁:是否按阻塞顺序拿锁。
4 可重入锁 or 不可重入锁: 拿锁线程是否可以多次拿锁。
4 synchronized的三种应用方式 4.1 synchronized修饰实例方法作用于给当前实例加锁,进入同步代码前要获得当前实例的锁。(注意:是当前实例,一个类有N个实例,一个实例有一把锁)。
使用synchronized 修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着,且共享对象一定是this,并且同步代码块是整个方法体。
示例1
public class HasSelfPrivateNum {
private int num = 0;
synchronized public void addI(String username){
try {
if ("a".equals(username)){
num = 100;
System.out.println("a set over! " + System.currentTimeMillis());
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b set over! " + System.currentTimeMillis());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("a");
}
}
public class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("b");
}
}
public class TestRun {
public static void main(String[] args) {
HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef1);
athread.start();
ThreadB bthread = new ThreadB(numRef2);
bthread.start();
}
}
运行结果如下:
synchronized numRef1 和 numRef2 是两个不同的类,同步方法的锁对象不是同一个对象,无法达到同步的目的,解决方法是让synchronized 锁对象是同一个,示例代码如下,运行结果如下图。
public class TestRun {
public static void main(String[] args) {
HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
//HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef1);
athread.start();
ThreadB bthread = new ThreadB(numRef1);
bthread.start();
}
}
运行结果如图:
4.2 synchronized作用于静态方法表示找类锁,类锁永远只有一把,就算创建了100个对象,那类锁也只有一把。
对象锁:一个对象一把锁,
类锁:100个对象,也是一把锁。
当synchronized 作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized 而线程B需要调用这个实例对象所属类的synchronized 方法,是允许的,不会发生互斥现象,因为访问静态synchronized 方法占用的锁是当前类的class对象而访问非静态synchronized 方法占用的锁是当前实例对象锁。
public class Service {
synchronized public static void printA(){
try {
System.out.println("线程名称为: " + Thread.currentThread().getName() + " " + System.currentTimeMillis() + " 进入printA ");
Thread.sleep(3000);
System.out.println("线程名称为: " +Thread.currentThread().getName() + " " + System.currentTimeMillis() + " 离开printA ");
}catch (InterruptedException e){
e.printStackTrace();
}
}
synchronized public static void printB(){
System.out.println("线程名称为: "+Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printB");
System.out.println("线程名称为: "+Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printB");
}
}
public class ThreadA extends Thread{
@Override
public void run() {
Service.printA();
}
}
public class ThreadB extends Thread{
@Override
public void run() {
Service.printB();
}
}
public class TestRun {
public static void main(String[] args) {
ThreadA a = new ThreadA();
a.setName("A");
a.start();
ThreadB b = new ThreadB();
b.setName("B");
b.start();
}
}
运行结果如下:
synchronized(class) 代码块的作用其实和synchronized static 方法的作用一样。
public class Service {
public static void printA(){
synchronized (Service.class){
try {
System.out.println("线程名称为: " + Thread.currentThread().getName() + " " + System.currentTimeMillis() + " 进入printA ");
Thread.sleep(3000);
System.out.println("线程名称为: " +Thread.currentThread().getName() + " " + System.currentTimeMillis() + " 离开printA ");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
synchronized public static void printB(){
System.out.println("线程名称为: "+Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入printB");
System.out.println("线程名称为: "+Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开printB");
}
}
public class ThreadA extends Thread{
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
service.printA();
}
}
public class ThreadB extends Thread{
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run() {
service.printB();
}
}
public class TestRun {
public static void main(String[] args) {
Service service1 = new Service();
Service service2 = new Service();
ThreadA a = new ThreadA(service1);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service2);
b.setName("B");
b.start();
}
}
运行结果如下:
synchronized 修饰代码块,指定加锁对象,对给定对向加锁,进入同步代码库前要获得给定对象的锁。在某些情况下,我们编写的方法体比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:
public class ObjectService {
public void serviceMethod(){
try {
synchronized (this){
System.out.println(Thread.currentThread().getName() + " begin time " + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " end time "+ System.currentTimeMillis());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadA extends Thread{
private ObjectService service;
public ThreadA(ObjectService service) {
this.service = service;
}
@Override
public void run() {
super.run();
service.serviceMethod();
}
}
public class ThreadB extends Thread{
private ObjectService service;
public ThreadB(ObjectService service) {
this.service = service;
}
@Override
public void run() {
super.run();
service.serviceMethod();
}
}
public class TestRun {
public static void main(String[] args) {
ObjectService service = new ObjectService();
ThreadA a = new ThreadA(service);
a.setName("a");
a.start();
ThreadB b = new ThreadB(service);
b.setName("b");
b.start();
}
}
运行结果如下:
synchronized(this) 代码块是锁定当前对象的,证明项目如下:
public class Task {
public void otherMethod(){
System.out.println(Thread.currentThread().getName() + "------------------run---otherMethod");
}
public void doLongTimeTask(){
synchronized (this){
for (int i = 0; i< 10000; i++){
System.out.println("synchronized threadName = " + Thread.currentThread().getName() + " i = " + (i + 1));
}
}
}
}
public class MyThread1 extends Thread{
private Task task;
public MyThread1(Task task) {
this.task = task;
}
@Override
public void run() {
super.run();
task.doLongTimeTask();
}
}
public class MyThread2 extends Thread{
private Task task;
public MyThread2(Task task) {
this.task = task;
}
@Override
public void run() {
super.run();
task.otherMethod();
}
}
public class TestRun {
public static void main(String[] args) throws InterruptedException {
Task task = new Task();
MyThread1 thread1 = new MyThread1(task);
thread1.start();
Thread.sleep(10);
MyThread2 thread2 = new MyThread2(task);
thread2.start();
}
}
运行结果如下:
修改Task.java如下:
public class Task {
synchronized public void otherMethod(){
System.out.println(Thread.currentThread().getName() + "------------------run---otherMethod");
}
public void doLongTimeTask(){
synchronized (this){
for (int i = 0; i< 10000; i++){
System.out.println("synchronized threadName = " + Thread.currentThread().getName() + " i = " + (i + 1));
}
}
}
}
运行结果如下:
多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。synchronized同步方法和synchronized(this)都是当前对象锁。
synchronized(非this对象)格式的作用只有一种:synchronized(非this对象x)同步代码块。
public class Service {
private String usernameParam;
private String passwordParam;
private String anyString = new String();
public void setUsernamePassword(String username, String password) {
try {
synchronized (anyString){
System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + "进入同步块");
usernameParam = username;
Thread.sleep(3000);
passwordParam = password;
System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + "离开同步块");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadA extends Thread{
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
super.run();
service.setUsernamePassword("a","a");
}
}
public class ThreadB extends Thread{
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run() {
super.run();
service.setUsernamePassword("b","b");
}
}
public class TestRun {
public static void main(String[] args) {
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("a");
a.start();
ThreadB b = new ThreadB(service);
b.setName("b");
b.start();
}
}
运行结果如下
锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这是虽然能实现同步,但会受到阻塞,影响执行效率;但如果使用同步代码块锁非this对象,则synchronized(非this) 代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,则可提高运行效率。
修改Service.java 文件代码如下:
public class Service {
private String usernameParam;
private String passwordParam;
public void setUsernamePassword(String username, String password) {
try {
String anyString = new String();
synchronized (anyString){
System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + "进入同步块");
usernameParam = username;
Thread.sleep(3000);
passwordParam = password;
System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + "离开同步块");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
运行结果如下:
使用synchronized(非this对象x)同步代码块格式进行同步操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用了,就会交叉运行。
5 synchronized底层实现原理(待补充) 6 Java三大变量的线程安全(待补充)实例变量:在堆中
静态变量:在方法区
局部变量:在栈中
局部变量在栈中,永远都不会存在线程安全问题,因为局部变量不共享。(一个线程一个栈)
实例变量在方法区中,方法区只有一个
堆和方法区都是多线程共享的,所以可能存在线程安全问题。



