【声明】:本篇文章来自本人gitee仓库搬运至CSDN,https://gitee.com/genmers/md-notes
引言
在开始记录之前,我来说说为什么需要学习这个关键字,以下是百度百科关于线程安全的一段描述
如果一个函数能够安全地同时被多个线程调用而得到正确的结果,那么,我们说这个函数是线程安全的。++所谓“安全”,一切可能导致结果不正确的因素都是不安全的调用。++
线程安全,是针对多线程而言的。与可重入联系起来,我们可以断定:可重入函数必定是线程安全的,但线程安全的不一定是可重入的。不可重入函数,函数调用结果不具有可再现性,可以通过互斥锁等机制使之能安全地同时被多个线程调用,那么,这个不可重入函数就是转换成了线程安全。
从第一句话我们就可以看出,线程安全无非就是要保证程序或者软件能在高负载的情况下依然不会出错
众所周知,同一进程的不同线程会共享同一主内存,而多个线程同时对共享内存读写时,若要保证线程安全,则必须通过加锁的方式。
Java中有两个主要的线程安全相关的关键字Synchronied和Volatile,今天我们来一起学习下Synchronied
一、Synchronized的两个用法
一句话说出Synchronized的作用
能够保证在同一时刻最多只有一个线程执行该段代码,以达到并发的安全效果。
对象锁
包括方法锁(默认锁对象为this即当前实例对象)和同步代码块(自己指定锁对象)
类锁
指syncronized修饰静态方法或者指定锁为Class对象。
在开始之前,我们先用一个例子来看看没有控制并发会有什么后果
栗子0:不控制并发的后果
package synchronizedknowedge;
/**
* 不开启线程同步的后果
*/
public class DisappearRequest1 implements Runnable{
static DisappearRequest1 instance = new DisappearRequest1();
static int i =0;
public static void main(String[] args) throws InterruptedException {
Thread t1 =new Thread(instance);
Thread t2 =new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
@Override
public void run() {
for (int j=0;j<10000;j++){
i++;
}
}
}
14178
按照我们执行一次10000来说,两个线程应该20000,得到的结果永远低于20000,不控制线程会引发一系列的并发问题
第一个用法:对象锁
- 代码块形式:锁的括号里的对象,对给定对象加锁,进入同步代码库前需要获得给定对象的锁
- 方法锁形式:synchronized修饰普通方法,锁对象默认为this
栗子1 : 同步代码块
package synchronizedknowedge;
/**
* 对象锁示例1 - 同步代码块
*/
public class SynchronizedObjectCodeBlock implements Runnable{
static SynchronizedObjectCodeBlock instance = new SynchronizedObjectCodeBlock();
@Override
public void run() {
synchronized (this) {
System.out.println("我是对象锁的代码块形式,我是"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束了");
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 =new Thread(instance);
Thread t2 =new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()){
Thread.sleep(3000);
}
System.out.println("finished");
}
}
运行结果
我是对象锁的代码块形式,我是Thread-0
//sleep3000ms
Thread-0结束了
我是对象锁的代码块形式,我是Thread-1
//sleep3000ms
Thread-1结束了
finished
而如果注释掉Synchronied,结果就会是
我是对象锁的代码块形式,我是Thread-0
我是对象锁的代码块形式,我是Thread-1
//sleep3000ms
Thread-1结束了
Thread-0结束了
finished
简单来说以上的情况就是,两个线程Thread-0,Thread-1都想执行同步代码块里的代码,但因为有Synchronied存在,一次只能一个对象获得锁(执行权)
而情况如果复杂点,就用自己定义的锁对象
栗子2:自定义锁对象
Object lock1 = new Object();
Object lock2 = new Object();
@Override
public void run() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName()+"拿到了lock1");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"的lock1结束了");
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName()+"拿到了lock2");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"的lock2结束了");
}
}
Thread-0拿到了lock1
//sleep3000ms
Thread-0的lock1结束了
Thread-0拿到了lock2
Thread-1拿到了lock1
//sleep3000ms
Thread-1的lock1结束了
Thread-0的lock2结束了
Thread-1拿到了lock2
//sleep3000ms
Thread-1的lock2结束了
finished
机制相同,不一样的是同步代码库变成两个,两个线程想去执行这两个代码就要先获取那个代码块的锁,同一时间同一代码块只有一个线程能拿到锁(执行权)
栗子3:普通方法锁
package synchronizedknowedge;
/**
* 对象锁的第二种形式 - 普通方法锁
*/
public class SynchroniedObjectMethod3 implements Runnable {
static SynchroniedObjectMethod3 instance = new SynchroniedObjectMethod3();
public static void main(String[] args) throws InterruptedException {
Thread t1 =new Thread(instance);
Thread t2 =new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()){
Thread.sleep(3000);
}
System.out.println("finished");
}
@Override
public void run() {
method();
}
public synchronized void method(){
System.out.println("我是对象锁的方法修饰符方式"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
}
我是对象锁的方法修饰符方式Thread-0
//sleep3000ms
Thread-0运行结束
我是对象锁的方法修饰符方式Thread-1
//sleep3000ms
Thread-1运行结束
finished
第二个用法:类锁
概念:Java类可能会有多个对象,但只有一个Class对象
类锁 = Class对象的锁
那类锁和对象锁的区别就是
对象的实例可以有很多个,而Class对象只有一个
就是说,对象锁说把同步(串形执行)的范围限定在制定的对象锁上,而如果对象锁有多个,理论上多个不同的对象锁还是可以同步执行同一个代码块
而类只有一个,就会在这个类的范围里同步
类锁的两种形式
形式1:synchronized加在static方法上
注意static无法直接重写在run方法上,因为不够
栗子4:在synchronied修饰的方法前加static
package synchronizedknowedge;
/**
* 类锁的第一种形式:static形式 static加在方法上
*/
public class SynchronizedClassStatic4 implements Runnable {
static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();
@Override
public void run() {
method();
}
public synchronized void method(){
System.out.println("我是类锁的第一种方法:static形式,我是"+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
无static修饰method,就会两个实例同时执行
我是类锁的第一种方法:static形式,我是Thread-0
我是类锁的第一种方法:static形式,我是Thread-1
//sleep3000ms
Thread-0运行结束
Thread-1运行结束
finished
如上可见,跟之前不同的是,对象的实例有两个,正常情况下因为是两个不同的实例即使用了同步代码块也没用
public synchronized void method(){
synchronized (this){
System.out.println("我是类锁的第一种方法:static形式,我是"+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
}
结果也没发生改变,这时候要实现串形执行就要在方法上加入static
public static synchronized void method(){
System.out.println("我是类锁的第一种方法:static形式,我是"+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
我是类锁的第一种方法:static形式,我是Thread-0
//sleep3000ms
Thread-0运行结束
我是类锁的第一种方法:static形式,我是Thread-1
//sleep3000ms
Thread-1运行结束
finished
形式2:synchronied(*.class)代码块
package synchronizedknowedge;
/**
* 类锁的第二种形式 synchronied(*.class)
*/
public class SynchroniedClassClass5 implements Runnable {
static SynchroniedClassClass5 instance1 = new SynchroniedClassClass5();
static SynchroniedClassClass5 instance2 = new SynchroniedClassClass5();
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
@Override
public void run() {
method();
}
private void method(){
synchronized (SynchroniedClassClass5.class){
System.out.println("我是类锁的第二种方法:synchronied(*.class)形式,我是"+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
}
}
我是类锁的第二种方法:synchronied(*.class)形式,我是Thread-0
//sleep3000ms
Thread-0运行结束
我是类锁的第二种方法:synchronied(*.class)形式,我是Thread-1
//sleep3000ms
Thread-1运行结束
finished
既然学完这几种方式,那我们用它来解决一下栗子0的问题
@Override
public synchronized void run() {
for (int j=0;j<10000;j++){
i++;
}
}
@Override
public void run() {
synchronized (this) {
for (int j=0;j<10000;j++){
i++;
}
}
}
@Override
public void run() {
synchronized (DisappearRequest1.class) {
for (int j=0;j<10000;j++){
i++;
}
}
}
以上几种方法都能解决栗子0的问题,由于run是实现Runnable接口的方法,我们不能擅自修改(增加static)
20000