马士兵高并发编程系列记录
https://www.bilibili.com/video/av11076511/?spm_id_from=333.788.videocard.4
public class T1 {
private int count = 10;
private Object o = new Object(); // o可以理解为监视器,谁要指向下面那个代码,先到o那里给o加锁,不然不能指向下面的代码
//o在堆内存new出来一个对象,对象可能有一些属性和方法,
public void m() {
synchronized(o) {
//任何线程要执行下面的代码,必须先拿到o的锁(是堆内存new出来一个对象,而不是o的引用)
//申请锁的时候,信息是记录在堆内存的对象
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
!!!!synchronized不是锁定整段代码,是执行代码的时候锁定当前对象
synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。
static synchronized方法也相当于全局锁,相当于锁住了代码段。
public class T2 {
private int count = 10;
public void m() {
synchronized(this) { //任何线程要执行下面的代码,必须先拿到this(自身)的锁
//synchronized锁定的是对象
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
public class T3 {
private int count = 10;
public synchronized void m() { //等同于在方法的代码执行时要synchronized(this)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
T2与T3等同
public class T4 {
private static int count = 10;
public synchronized static void m() { // static 静态,这里等同于synchronized(T.class)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void mm() {
synchronized(T4.class) {
// T4.class 是Class类的一个对象
//考虑一下这里写synchronized(this)是否可以? 不可以 静态的属性和方法是不需要new出对象的,没有对象所以没有this,所以锁定的是该类的class对象
count --;
}
}
}
public class T5 implements Runnable {
private int count = 10;
/*
public void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
不加synchronized可能会出现数据不一致的问题,比如结果为7 5 6 7 7
线程重入:
线程1在运行到count--还没有执行下一句的打印语句的时候被线程2打断,
线程2在运行到count--还没有执行下一句的打印语句的时候被线程3打断,线程3执行count--以及打印语句打印出7。。。
要想得到想要的结果9,8,7,6,5
加锁synchronized,某个线程在执行这段代码的时候不能被打断
如下面的run()方法
*/
//一个synchronized代码块,相当于一个原子操作,说明该段代码不可分,某个线程在执行这段代码的时候不能被打断,只有这个线程执行完,其他线程才能执行这段代码
public synchronized void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
T5 t = new T5();//只new了一个对象,好多线程共同访问这一个对象,并不是每个线程都new一个对象
//栈内存里有一个t,指向堆内存new一个T对象,
for(int i=0; i<5; i++) {
//开启了5个线程,每个线程都访问t里面的run方法,访问的count都是堆内存new一个T对象里面记录的count
new Thread(t, "THREAD" + i).start();//创建线程并命名,启动线程
}
}
}
与上一个相同
public class T6 implements Runnable {
private int count = 10;
public synchronized void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
for(int i=0; i<5; i++) {
T6 t = new T6();
new Thread(t, "THREAD" + i).start();
}
}
}
/**
* 同步和非同步方法是否可以同时调用? 可以
* @author mashibing
*/
public class T7 {
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " m1 start...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1 end");
}
public void m2() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m2 ");
}
public static void main(String[] args) {
T7 t = new T7();
//lamada表达式 java8特性
new Thread(()->t.m1(), "t1").start();
new Thread(()->t.m2(), "t2").start();
//synchronized是重入锁
//只有synchronized这样的方法在执行的时候才需要这把锁,其他方法不需要这把锁
// new Thread(t::m1, "t1").start();
// new Thread(t::m2, "t2").start();
//这两句与上面一样
/*
new Thread(t::m1 相当于
new Thread(new Runnable() {
@Override
public void run() {
t.m1();
}
});
*/
}
}
/**
* 对业务写方法加锁
* 对业务读方法不加锁
* 容易产生脏读问题(dirtyRead)
*/
import java.util.concurrent.TimeUnit;
public class Account {
String name;
double balance;
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
public double getBalance(String name) {
return this.balance;
}
public static void main(String[] args) {
Account a = new Account();
new Thread(()->a.set("zhangsan", 100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
}
}
/*
结果是0.0 100.0
对写进行加锁,对读没有加锁,可能出现读到在写的过程中还没有完成的数据,造成脏读
脏读(一个事务读取到了另外一个事务没有提交的数据 )
写线程加锁,读线程没有加锁,加入A线程在写,BC线程在读,B读的是写之前的数据(sleep两秒钟之前),C读的是写之后的数据(sleep两秒钟之后),所以读出的结果不一致
set函数中sleep两秒钟是因为放大线程之前的时间差
将sleep代码注释掉后,两次读出的数据一致,因为中间没有sleep马上账户余额设置为100
//try {
// Thread.sleep(2000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
问题的解决:对读也进行加锁
public synchronized double getBalance(String name) {
return this.balance;
}
*/
一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
也就是说synchronized获得的锁是可重入的 (获得锁之后还可以再获得一遍)
import java.util.concurrent.TimeUnit;
public class T9 {
//同一个线程是可以的,因为是重入锁
synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
}
synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
}
一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
也就是说synchronized 获得的锁是可重入的
这里是继承中有可能发生的情形,子类调用父类的同步方法
import java.util.concurrent.TimeUnit;
public class T10 {
synchronized void m() {
System.out.println("m start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m end");
}
public static void main(String[] args) {
new TT().m();
}
}
class TT extends T10 {
@Override
synchronized void m() {
System.out.println("child m start");
super.m();
System.out.println("child m end");
}
}
程序在执行过程中,如果出现异常,默认情况锁会被释放
所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
因此要非常小心的处理同步业务逻辑中的异常
import java.util.concurrent.TimeUnit;
public class T11 {
int count = 0;
synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start");
//死循环
while(true) {
count ++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count == 5) {
int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
System.out.println(i);
}
}
}
public static void main(String[] args) {
T11 t = new T11();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}
}
volatile 关键字,使一个变量在多个线程间可见
A B 线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
使用volatile关键字,会让所有线程都会读到变量的修改值
在下面的代码中,running是存在于堆内存的t对象中
当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
使用volatile,将会强制所有线程都去堆内存中读取running的值
可以阅读这篇文章进行更深入的理解
http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
synchronized既有可见性又保持原子性,volatile只是保持可见性
volatile 当改了主内存中的值后会通知其他线程重新读一下
过期缓存通知
不加上volatile,线程之间不可见
当两个线程共同访问的变量要加volatile,保持线程之间的可见性,不然要加synchronized,synchronized太重了,效率比较低,volatile效率比较高
能用volatile就不要加锁,程序的并发性可以提高很多
import java.util.concurrent.TimeUnit;
public class T12 {
/*volatile*/ boolean running = true;
//对比一下有无volatile的情况下,整个程序运行结果的区别
//不加 m start ,加上 m start m end!
void m() {
System.out.println("m start");
while(running) {
/*
// 不加sleep语句,cpu特别忙,不会去主内存刷新running的值,加上sleep,可能cpu有空闲,有可能回去主内存刷新一下running的值
// 当两个线程共同访问的变量要加volatile,保持线程之间的可见性,不然要加synchronized,synchronized太重了,效率比较低,volatile效率比较高
//能用volatile就不要加锁,程序的并发性可以提高很多
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/
}
System.out.println("m end!");
}
public static void main(String[] args) {
T12 t = new T12();
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}
}
volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
synchronized既有可见性又保持原子性,
volatile只是保持可见性(一个值修改后另一个可见)
链接:https://www.zhihu.com/question/49656589/answer/117826278
volatile 只能保证 “可见性”,不能保证 “原子性”。count++; 这条语句由3条指令组成:
(1)将 count 的值从内存加载到 cpu 的某个寄存器r
(2)将 寄存器r 的值 +1,结果存放在 寄存器s
(3)将 寄存器s 中的值写回内存所以,如果有多个线程同时在执行 count++;,
在某个线程执行完第(3)步之前,其它线程是看不到它的执行结果的。在没有 volatile 的时候,执行完 count++;,
执行结果其实是写到CPU缓存中,没有马上写回到内存中,后续在某些情况下(比如CPU缓存不够用)再将CPU缓存中的值flush到内存。
正因为没有马上写到内存,所以不能保证其它线程可以及时见到执行的结果。在有 volatile 的时候,执行完 count++;,
执行结果写到CPU缓存中,并且同时写回到内存,因为已经写回内存了,所以可以保证其它线程马上看到执行的结果。
但是,volatile 并没有保证原子性,在某个线程执行(1)(2)(3)的时候,volatile 并没有锁定 count 的值,
也就是并不能阻塞其他线程也执行(1)(2)(3)。可能有两个线程同时执行(1),所以(2)计算出来一样的结果,然后(3)存回的也是同一个值。
import java.util.ArrayList;
import java.util.List;
public class T13 {
volatile int count = 0;
void m() {
for(int i=0; i<10000; i++)
count++;
}
public static void main(String[] args) {
T13 t = new T13();
List threads = new ArrayList();
for(int i=0; i<10; i++) {
/*十个线程共同访问一个变量count(加volatile,保证了可见性),每个线程往上加10,000个数,正常应该是100,000
A线程现在count是100,B读走现在的值是100,B加一写回去是101,A加一写回去也是101,将B的101覆盖了一遍,
此时假如C去读取,读取到101(可见性),加入C去加1,写回去的时候不去检查
*/
threads.add(new Thread(t::m, "thread-"+i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
解决 去掉volatile,在方法前加synchronized.(也是第14个例子)
import java.util.ArrayList;
import java.util.List;
public class T13_modify {
int count = 0;
synchronized void m() {
for(int i=0; i<10000; i++)
count++;
}
public static void main(String[] args) {
T13_modify t = new T13_modify();
List threads = new ArrayList();
for(int i=0; i<10; i++) {
threads.add(new Thread(t::m, "thread-"+i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
解决同样的问题的更高效的方法,使用AtomicXXX类
AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class T15 {
AtomicInteger count = new AtomicInteger(0);
void m() {
for (int i = 0; i < 10000; i++)
//if count.get() < 1000
count.incrementAndGet(); //替代count++(不具有原子性); incrementAndGet是原子方法,效率比synchronized高很多
}
public static void main(String[] args) {
T15 t = new T15();
List threads = new ArrayList();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));//创建线程
}
threads.forEach((o) -> o.start());//JDK1.8新特性 启动线程
threads.forEach((o) -> {
try {
o.join();//等线程执行完毕之后才执行主线程main
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class T15_add {
AtomicInteger count = new AtomicInteger(0);
void m() {
for (int i = 0; i < 10000; i++) {
if (count.get() < 1000) //count.get() 具有原子性
count.incrementAndGet();// count.incrementAndGet() 具有原子性
/*
count.get() 具有原子性,count.incrementAndGet() 具有原子性
但是在两句话的中间没有原子性 加入A线程读取到999,判断count<1000还没有加一,但是B线程读取到999,判断count<1000加1到1000了,
此时A线程继续执行(已经判断完)加1结果为1001
*/
}
}
public static void main(String[] args) {
T15_add t = new T15_add();
List threads = new ArrayList();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
synchronized优化
同步代码块中的语句越少越好
比较m1和m2
import java.util.concurrent.TimeUnit;
public class T16 {
int count = 0;
synchronized void m1() {//整个加synchronized 粗粒度的锁
//do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
count ++;
//do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void m2() {
//do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
//采用细粒度的锁,可以使线程争用时间变短,从而提高效率
synchronized(this) {
count ++;
}
//do sth need not sync
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
锁定某对象o,如果o的属性发生改变,不影响锁的使用
但是如果o变成另外一个对象,则锁定的对象发生改变
应该避免将锁定对象的引用变成另外的对象
import java.util.concurrent.TimeUnit;
public class T17 {
Object o = new Object();// 栈内有小内存,指向堆中对象真正存放的地方,锁定某对象o是锁定真正的对象(堆内存中)而非引用(栈内存中)
void m() {
synchronized(o) {
while(true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
T17 t = new T17();
//启动第一个线程
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建第二个线程
Thread t2 = new Thread(t::m, "t2");
t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
t2.start();
}
}
运行结果:
t1
t1
t1
t2
t1
t2等等
分析结果:相当于两个在堆内存里的对象都有锁,两个线程各获取两个对象上的锁,互相不影响
不要以字符串常量作为锁定对象(锁的不是引用)
在下面的例子中,m1和m2其实锁定的是同一个对象,所以下面程序看似是两把锁,其实是同一把锁。
这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,
但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,
因为你的程序和你用到的类库不经意间使用了同一把锁
jetty出现过这个bug
public class T18 {
String s1 = "Hello";
String s2 = "Hello";
void m1() {
synchronized(s1) {
}
}
void m2() {
synchronized(s2) {
}
}
}