目录
3.synchronized也可以给代码重排序增加一定的约束
提出锁的原因
为了解决线程安全问题,需要一些机制,目标和JVM沟通,避免线程不安全的问题发生。
常见类的线程安全问题:ArrayList,LinkedList,PriorityQueue.TreeMap,TreeSet,HashMap,HashSet 都不是线程安全的。例如,ArrayList为什么不是线程安全的?——多个线程同时对一个ArrayList对象有修改操作时,结果会出错。从线程不安全的角度看原因:1.有没有共享数据?ArrayList 对象共享array属性和size属性。2.多个线程有没有写操作?有修改操作,array[size]=num;size++;都是修改。3.原子性或者可见性或代码重排序。
为了方便理解:static boolean lock=false;锁有两种状态:锁上状态(locked)和打开状态(unlocked)。
synchronized锁(同步锁)
1.语法:synchronized(引用->指向的就是被当成锁的对象){请求锁.. 临界区(被锁保护起来的代码)..释放锁} //JVM针对每个对象都实现了锁的功能
请求锁:请求成功(该锁没有线程持有)继续大括号内代码的执行;请求失败(该锁被线程持有),请求锁的线程会阻塞,直到锁被释放,重新去请求锁。
临界区:加锁中的代码。
static Object lock=new Object();
static class MyThread extends Thread{
@Override
public void run() {
synchronized (lock){
//临界区代码
for (int i = 0; i < 10000; i++) {
System.out.println("张三");
}
}
//非临界区代码
for (int i = 0; i < 10000; i++) {
System.out.println("李四");
}
}
}
synchronized修饰方法
a. synchronized修饰普通方法:视为对“当前对象(使用哪个对象调用这个同步方法)”加锁 -> synchronized(this){...};
b. synchronized修饰静态方法:视为对静态方法所在的类加锁 -> synchronized(类.class){...};
代码演示:
public class Main {
synchronized void m1() {
//同步方法
//视为对“当前对象(使用哪个对象调用这个同步方法)”加锁
}
synchronized static void m2() {
//同步静态方法
//视为对静态方法所在的类加锁
}
void m3() {
}
void m4() {
synchronized (this) {
}
}
void m5() {
synchronized (Main.class) {
}
}
Object o1 = new Object();
void m6() {
synchronized (o1) {
}
}
static Object o2 = new Object();
void m7() {
synchronized (o2) {
}
}
public static void main(String[] args) {
// s1=new Main();
// s1=new Main();
// s3==s1;
/**
* t1线程 t2线程 是否互斥
* s1.m1():加锁,this==s1 s1.m1():加锁,this==s1 互斥
* s1.m1():加锁,this==s1,s1==s3 s3.m1():加锁,this==s3 互斥
* s1.m1():加锁 s2.m1():加锁,s2 不互斥s1!=s2
* s1.m1():加锁,this==s1 s1.m2():加锁,Main.class 不互斥
* s1.m1():加锁,s1指向的对象 s1.m3():没有加锁 不互斥
* s1.m1():加锁,s1指向的对象 s1.m4():加锁 互斥
* s1.m1():加锁,s1指向的对象 s3.m4():加锁 互斥
* s1.m1():加锁,s1指向的对象 s1.m5():加锁,静态方法所在类 不互斥
* s1.m2():加锁,静态方法所在类 s1.m5():加锁,静态方法所在类 互斥
* s1.m1():加锁,s1指向的对象 s1.m6():加锁,o1对象 不互斥
* s1.m6():加锁,s1.o1对象 s2.m6(): 加锁,s2.o1对象 不互斥
* s1.m6():加锁,s1.o1对象 s3.m6():加锁,s3.o1对象,s3和s1指向同一个对象 互斥
* s1.m2():加锁,Main.class对象 s1.m7():加锁,Main.o2对象 不互斥
* s1.m7():加锁,Main.o2对象 s1.m7():加锁,Main.o2对象 互斥
* s1.m7():加锁,Main.o2对象 s3.m7():加锁,Main.o2对象 互斥
* s1.m7():加锁,Main.o2对象 s2.m7():加锁,Main.o2对象 互斥
*/
}
}
JVM和锁
JVM在设计对象时,在对象内部都有一个moniter(监视器),每个对象都有。JVM就可以把每个对象都当成锁使用。这个设计其实不太好,synchronized(引用){...}当引用==null时,隐含一个解引用的操作(dereference)的操作(通过引用操作引用指向的对象)。所以如果为空,会报空指针异常(NullPointerExpection)。
加锁操作
使得线程互斥(synchronized和我们配合,正确使用synchronized),保证线程安全。
public class Main {
// 这个对象用来当锁对象
static Object lock = new Object();
static class MyThread1 extends Thread {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 1_0000; i++) {
System.out.println("我是张三");
}
}
}
}
static class MyThread2 extends Thread {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 1_0000; i++) {
System.out.println("我是李四");
}
}
}
}
public static void main(String[] args) {
Thread t1 = new MyThread1();
t1.start();
Thread t2 = new MyThread2();
t2.start();
}
}
1.保证了临界区的原子性
以下两种加锁操作,加锁的“粒度”不同,第一种加锁粒度较细,第二种加锁粒度较粗,粒度不是越细或者越粗越好,是一个需要工程测量的取值,粗略的说:加锁粒度越细,并发可能性越高。粒度:临界区的代码多少。
for (int i = 0; i < COUNT; i++) {
//粒度较细 加锁r++释放锁(循环COUNT次)
synchronized (sync.class){
r++;
}
}
//粒度较粗 加锁 r++(循环COUNT次) 释放锁
synchronized (sync.class){
for (int i = 0; i < COUNT; i++) {
r--;
}
}
注意:只要加锁就要加锁失败的概率,线程阻塞。 而且只能保证r++和r--互斥,不能保证先后顺序。
下面这种写法是做不到互斥的(原子性的保证),不是一把锁,t1线程加的t1指向的对象,t2线程加的t2指向的对象:
static class Add extends Thread {
@Override
//同步锁 this指向 this->Add线程对象
public synchronized void run() {
for (int i = 0; i < COUNT; i++) {
r++;
}
}
}
static class Sub extends Thread {
@Override
//同步锁 this指向 this->Add线程对象
public synchronized void run() {
for (int i = 0; i < COUNT; i++) {
r--;
}
}
}
2.synchronized在有限程度上可以保证内存可见性
加锁-临界区代码-解锁:加锁成功之前,清空当前线程的工作内存。临界区代码读某些变量时(主内存上的数据时),保证读到的是最新的(“最新”是有限度的最新)。解锁时,保证把工作内存中的数据全部同步回主内存。
但是,临界区期间的数据读写,不做保证:1.可能读的数据,再次被别的线程更改了,就看不到了。2.期间释放有数据同步回主内存。
所以synchronized在“有限程度上,保证内存可见性。
3.synchronized也可以给代码重排序增加一定的约束
s1;s2;s3;加锁;s4;s5;s6;解锁;s7;s8;s9;
s1;s2;s3;之间不做保证,s4;s5;s6之间不做保证,s7;s8;s9;之间不做保证。
但可以保证的是:s4;s5;s6不会被重排序到加锁前面,也不会重排序到解锁之后。s1;s2;s3也不会重排序到加锁之后,s7;s8;s9;不会重排序到解锁之前。