多线程之Synchronized学习笔记
Synchronized简介
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法和 synchronized 块。
以上来自百度结果,好啰嗦,接下来简单阐述。
作用:能够保证在 同一时刻 最多只有 一个线程 执行该段代码,已达到保证并发安全的效果。
地位:是 Java 的关键字,被 Java 语言原生支持;是最基本的互斥同步手段;是并发编程中的元老级角色,是并发编程的必学内容。
不使用并发手段的后果:
经典例子:两个线程同时进行对 a 进行 a++,在给定一定范围循环,理论来说 a 的结果是确定的(循环次数相同,a++ 次数相同,一直累加即可)。 但实际上 a 的值会比预计的小很多,因为可能两个线程同时拿到锁,即同时拿到 a 对其进行操作,导致结果出错。
使用 synchronized 可以解决。
Synchronized 用法
1.对象锁
包括 方法锁 (默认锁对象为 this 当前实例对象)和 同步代码块锁(自己指定锁对象)
代码块形式:手动指定锁对象
public void test1() {
synchronized(this) {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
}
catch (InterruptedException ie) {
}
}
}
}
方法锁形式:synchronized 修饰普通方法,锁对象默认为 this
public synchronized void test2() {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
}
catch (InterruptedException ie) {
}
}
}
main方法
public static void main(String[] args)
{
final TestSynchronized myt2 = new TestSynchronized();
Thread test1 = new Thread( new Runnable() { public void run() { myt2.test1(); } }, "test1" );
Thread test2 = new Thread( new Runnable() { public void run() { myt2.test2(); } }, "test2" );
test1.start();;
test2.start();
}
2.类锁
指 Synchronized 修饰 静态 的方法或指定锁为 Class对象
O_o Java类可能有很多个对象,但只有一个 Class对象
本质:所谓的类锁,是 Class对象的锁
形式1:synchronized 加在 static 方法上
public static synchronized void test1() {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
}
catch (InterruptedException ie) {
}
}
}
O^O注意不能给 run() 方法加 static,应该在 run() 中再调用另外写的 static 方法
形式2:synchronized(.class) 代码块**
public void test2() {
synchronized(TestSynchronized.class) {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
}
catch (InterruptedException ie) {
}
}
}
}
main方法
public static void main(String[] args) {
final TestSynchronized myt2 = new TestSynchronized();
Thread test1 = new Thread( new Runnable() { public void run() { myt2.test1(); } }, "test1" );
Thread test2 = new Thread( new Runnable() { public void run() { TestSynchronized.test2(); } }, "test2" );
test1.start();
test2.start();
}
多线程访问同步方法的 7 种情况
1.两个线程同时访问一个对象的同步方法
竞争争抢锁,相互等待,不能同时持有
2.两个线程访问的是两个对象的同步方法
并行执行,互不影响,拥有的锁对象不是同一个
3.两个线程访问的是 synchronized 的静态方法
静态方法对应的锁对象同一个 类锁,所以需要相互等待
4.同时访问同步方法与非同步方法
同时开始,同时结束。synchronized 关键字只影响被修饰的方法,而不会去影响 非同步方法。(两个线程访问不同方法)
5.访问同一个对象的不同的普通同步方法
持有的是同一把锁,所以需要相互等待
6.同时访问静态synchronized 和 非静态synchronized 方法(复杂)
持有锁对象不同。一个是类锁,一个是对象锁。所以互不影响,各自运行
7.方法抛出异常后,会释放锁
问题:如果调用了一个 synchronized 方法,里面又调用了一个非 synchronized 方法,安全吗?
答:不安全,因为该非同步方法可能会被不同线程调用,当某一线程在调用时可能另一线程恰巧也在调用,可能造成操作失败数据混乱。
Synchronized性质
可重入性质
指同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁
好处:避免死锁,提升封装性
粒度:线程而非调用
情况1:同一个方法是可重入的
//可重入粒度测试:递归调用本方法
static class SynchronizedRecurision{
int a = 0;
public static void main(String[] args) {
SynchronizedRecurision s = new SynchronizedRecurision();
s.method1();
}
private synchronized void method1(){
System.out.println("这是method1,a="+a);
if(a==0){
a++;
method1();
}
}
情况2:可重入不要求是同一个方法
//可重入粒度测试:调用不同方法
static class SynchronizedOtherMethod{
public static void main(String[] args) {
SynchronizedOtherMethod s = new SynchronizedOtherMethod();
s.method1();
}
public synchronized void method1(){
System.out.println("我是 method1");
method2();
}
public synchronized void method2(){
System.out.println("我是 method2");
}
}
情况3:可重入不要求是在同一个类中
//可重入粒度测试:调用其他类的方法
static class SynchronizedSuperClass{
public synchronized void doSomething(){
System.out.println("我是父类方法");
}
static class TestClass extends SynchronizedSuperClass{
@Override
public synchronized void doSomething(){
System.out.println("我是子类方法");
super.doSomething();
}
}
public static void main(String[] args) {
TestClass t = new TestClass();
t.doSomething();
}
}
总结:只要调用的方法他所需要的锁是当前拥有的锁,就可以直接使用。
不可中断性质
一旦这个锁已经被别人获得,如果我还想获得,那我只能选择等待或者阻塞,直到其他线程释放这个锁。如果其他线程永远不释放锁,那么我只能永远等待。
(相比之下 Lock 类更灵活,拥有中断的能力。如果觉得等待时间过长,有权中断现在已经获取到锁的线程的执行;或者当等待时间太长,不想等待可以主动选择退出。)
加锁、释放锁原理
以下两个方法等价
可重入原理:加锁次数计数器
JVM负责跟踪对象被加锁的次数
线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象上再次获得锁时,计数会递增;每当任务离开时,计数递减,当计数减为0时,锁被完全释放。
保持可见性原理:内存模型
Synchronized的缺陷
效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断应该正在试图获得锁的线程。
不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。
无法知道是否成功获取到锁。
相比之下,Lock 类功能更加强大:
lock(), unlock(), tryLock/tryLock(10,TimeUnit.SECONDS)设置超时时间,等等方法
Synchronized 与 ReentrantLoc的区别
ReentrantLock(可重入锁)
-
synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
-
synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;
ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。 -
synchronized不可响应中断,一个线程获取不到锁就一直等着;
ReentrantLock可以响应中断。 -
ReentrantLock还可以实现公平锁机制。
就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
常见问题
1.使用注意点:锁对象不能为空、作用域不宜过大,避免死锁;
2.如何选择 Lock 和 Synchronized 关键字?
尽量避免使用,如果有现成的工具包就使用工具包;
若没有,就使用 synchronized,减少代码编写,减少出错;
实在需要 Lock,再用 Lock(加解锁机制)。
3.多线程访问同步方法的各种具体情况
前面提到的 7 种情况。
4.多个线程等待同一个 synchronized锁时,JVM如何选择下一个获取锁的是哪个线程
内部锁调度机制,一个持有锁的线程释放锁之后,除了事先等待的线程,还有恰好刚申请锁的线程(还没进入 Block状态),JVM 的选择是随机、不可控制的。
5.Synchronized 使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能
优化使用范围(synchronized 的修饰范围);
使用 Lock。
6.想更灵活地控制锁的获取和释放(现在释放锁的时机都被规定死了)应该如何做
自己实现一个 Lock 接口,编写对应的功能实现方法。
Synchronized 总结
JVM 会自动通过使用 monitor 来加锁和解锁,保证了同一时间只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。