Java synchronized笔记

简介:

1,造成线程安全问题的原因
①存在线程间的共享数据(例如堆内存)
②存在多个线程同时操作共享数据

2,锁类型
互斥锁:(重量级锁)同一时刻有且仅有一个线程操作共享数据,当某个线程正在操作共享数据时,其他线程处于等待状态,必须等到该线程处理完后再进行.
偏向锁:java1.6的新锁,针对多个线程竞争较少的场景,如果一个线程获得了锁,那么锁就进入偏向模式,对象头Mark Word里面的锁结构也编程偏向锁结构,这个线程再次请求锁时,无需做任何操作。
轻量级锁:依据是大部分的锁,整个同步周期内不存在竞争,适用线程交替执行同步块的场景。
自旋锁:依据是大部分线程持有锁的时间不会太长,因此自旋锁假设很快当前线程就会获得锁,所以虚拟机会让想要获取锁的线程执行几个空循环,如果循环完成后得到锁,则顺利进入。否则就会将线程在操作系统层面挂起。
锁消除:指虚拟机在编译时消除无用的锁。

3,synchronized关键字作用:
①保证同一时刻只有一个线程可以执行某个方法或者代码块
②可以保证一个线程操作的共享数据变化能立即被其他线程所见(可替代volatile)

4,synchronized关键字用法:
①修饰实例方法:作用于当前实例的方法(不包括静态方法),进入同步代码前要获取当前实例的锁(JVM通过常量池中的方法结构表的acc_synchronized标识区分一个方法是否是同步方法。)
public class SynchronizedClass extends Thread
{

static int share = 0;
//①修饰实例方法
public synchronized void increase()
{
    share++;
}

//②修饰静态方法
public synchronized static void increase4ClassLock(){
    share++;
}
public void run()
{
    for (int i = 0; i < 1000; i++)
    {
        increase();
    }
}

/**
 * @param args
 */
public static void main(String[] args)
{
    try
    {
        //场景1,t1,t2线程执行需要获得实例sc1的互斥锁
        SynchronizedClass sc1 = new SynchronizedClass();
        Thread t1 = new Thread(sc1);
        Thread t2 = new Thread(sc1);
        t1.start();
        t2.start();
        // join()方法:当前线程结束之后join()方法才返回,如果不试用join,打印出来的数据可能是0,1000,2000
        t1.join();
        t2.join();
        System.out.println("scene1 share:" + share);
        
        //share = 0;
        //场景2,t3,t4线程执行各自获得实例sc2,sc3的锁,可能同时访问共享的静态资源share出现线程安全问题
        SynchronizedClass sc2 = new SynchronizedClass();
        SynchronizedClass sc3 = new SynchronizedClass();
        Thread t3 = new Thread(sc2);
        Thread t4 = new Thread(sc3);
        t3.start();
        t4.start();
        t3.join();
        t4.join();
        System.out.println("scene2 share:" + share);
    } catch (Exception e)
    {
        e.printStackTrace();
    }
}

}
上面例子中:当前线程的锁是实例对象instance,java的线程同步锁可以是任意对象。注意:如果一个线程正在访问一个实例instance的synchronized方法A时,其他线程不能访问该实例的其他synchronized方法,因为一个对象实例只有一把锁,但其他线程可访问该实例的非synchronized方法。当然,如果其他线程要访问另外一个实例instance1的任何方法都是不受限制的,但是如果他们访问的是共享数据那么就会出问题,例如上面例子场景2中的静态变量share,这时候就需要使用②进行修饰。
②修饰静态方法:作用于当前类对象,进入同步代码前要获得当前类(class)对象的锁。注意:两个线程可以同时分别访问static synchronized修饰的方法和non-static synchronized修饰的方法,因为他们一个是类对象的锁,一个是实例对象的锁,但是此时两个方法如果同时操作共享数据,也可能出现线程安全问题。例如上面例子中两个线程分别同时访问:increase4ClassLock()和increase()方法,就会出现线程安全问题。
③修饰代码块:指定加锁对象,给指定对象家锁,进入同步代码块前要获取指定对象的锁(可以是类对象,也可以是实例对象),样例如下:
static SynchronizedClass inst1 = new SynchronizedClass();
public void increase2MethodBlock1(){

//使用实例锁,获取当前对象实例的锁,inst1也可用this(synchronized (inst1))
synchronized (inst1)
{
    share++;
}

}
public void increase2MethodBlock2(){

//使用类对象锁,获取当前class对象的锁
synchronized (SynchronizedClass.class)
{
    share++;
}

}

5,sychronized实现原理
jvm中synchronized的同步是通过管程(Monitor)实现的,在java的堆内存中,一个对象实例信息是按照如下方式存储的:

image

如上如所示结构,java对象头里面有锁信息,锁信息里有一个指向monitor(管程)对象的指针,管程对象在java虚拟机中是使用OjbectMonitor类实现的,类结构大致如下:
ObjectMonitor(){
_count = 0; //计数器
_WaitSet = null;
_EntryList = null;
_owner = null; //指向拥有ObjectMonitor对象的线程
……
}
说明:
①多个线程进入同步代码时,会先进入_ EntryList队列,获取到monitor对象后修改owner指向为当前对象线程,计数器加1
②线程调用wait()方法进入_WaitSet队列,owner置为空,count减1
③当前线程执行完毕也会重置owner为空,复位变量
image

6,synchronized需要注意的点
①可重入性
一个线程在synchronized方法体内部可以调用另外一个synchronized方法,因为是同一个线程请求同一个锁。子类继承父类时,子类可通过重入锁调用父类的synchronized方法。
②在线程sleep时调用interrupt方法会打断阻塞并抛出异常(对阻塞线程进行中断操作):
image

③处于非阻塞状态的线程需要我们手动进行中断检测并结束程序:
image

如上②和③两种情况(阻塞/和线程运行时中断中断的操作方法是不一样的[前者要捕捉异常,后者要手动判断中断状态并做中断处理]),可用下面代码兼顾两种场景:
image

④线程状态转换
image
1)等待阻塞:等待阻塞(使用wait,notify/notifyAll方法进入和退出)(wait是Object类的方法)
2)锁定阻塞:synchronized同步锁的获得和释放
3)其他阻塞:sleep,join,IO操作等(sleep是Thread类的静态方法)
4)就绪状态获得时间片就进入运行态,时间片用完或yield操作回到就绪状态
5)sleep和wait的区别:sleep方法是Thread类的今天方法而wait是Object类的方法;sleep方法不会释放对象锁,sleep时其他线程任然不能访问同步块,而wait方法会释放对象锁。
image
⑤线程中断操作对正在等待获取锁的synchronized方法或者代码块是不起作用的:一个正在等待锁的线程要么获得锁继续执行,要么继续等待。
image

⑥wait(); notify(); notifyAll();方法必须放在synchronized中,否则会抛出IllegalMonitorStateException异常,因为:调用这几个方法必须获取管程(ObjectMonitor)对象,该对象放在对象头中,而synchronized可以获取到该对象。

目录
相关文章
|
2月前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
本系列教程笔记详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。若需快速学习Kotlin,建议查看“简洁”系列教程。本期重点介绍了Kotlin与Java的共存方式,包括属性、单例对象、默认参数方法、包方法、扩展方法以及内部类和成员的互操作性。通过这些内容,帮助你在项目中更好地结合使用这两种语言。
48 1
|
2月前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
34 2
|
9天前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
9天前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
16天前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
25 2
|
9天前
|
Java 数据库连接 编译器
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
26 0
|
1月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
44 4
|
1月前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
1月前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
1月前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编