一、synchronized
1、synchronized原理
一个synchronized代码块,相当于一个原子操作,原子是不可分的,在线程执行代码块的时候,持有这把锁,在执行这段代码块的时候不可能被打断,执行结束之后其他线程才能继续执行同一段代码。
2、类锁和对象锁的区别
对象锁: 锁在堆内存里的那个对象。同一个对象再次遇到同步代码时,需要等待其他线程执行完毕,才能继续执行。
如果一段代码在开始的时候就synchronized(this),到结束时才释放锁,可以直接写在方法声明上。不是锁定那段代码,而是锁定当前对象。
类锁: 锁在类的class对象上。同一个class执行到同步代码时,会被锁定,执行完被锁定的代码,下一个class对象才能执行。
public class T {
private int count = 10;
public void m() {
//任何线程要执行下面的代码,必须先拿到this的锁
synchronized(this) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
//等同于在方法的代码执行时要synchronized(this)
public synchronized void m() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
3、同步方法和非同步方法可以同时调用
public class Test02 {
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " m1开始执行。。。。");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1结束。。。。");
}
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) {
Test02 test = new Test02();
new Thread(()-> test.m1()).start();
new Thread(()->test.m2()).start();
}
}
4、脏读:只是对写的方法加锁,对读的方法没有加锁。写方法执行过程中,读方法可以执行,读到的数据可以还没有被修改,就会产生脏读。具体业务中,要看能不能脏读(性能比读写都加锁好)。
new Thread(()->account.set("zhangsan", 100)).start();启动一个线程,1ms后,主线程启动,去读取balance,此时匿名线程还没开始修改balance的值。10000ms后,balance的值被修改,此时再去读取balance的值,就是100了。
public class Account {
private String name;
private double balance;
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
public double getBalance() {
return this.balance;
}
public static void main(String[] args) throws InterruptedException {
Account account = new Account();
new Thread(()->account.set("zhangsan", 100)).start();
Thread.sleep(1);
System.out.println(Thread.currentThread().getName() + account.getBalance());
Thread.sleep(10000);
System.out.println(Thread.currentThread().getName() + account.getBalance());
}
}
5、synchronized锁可重入。一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。
调用 t 对象的m1方法,需要对 t 加锁,锁定过程中,去调用m2,发现m2也需要锁,而这个锁就是当前自己已经持有的锁。
public class Test03 {
public synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
}
public synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
public static void main(String[] args) {
Test03 test03 = new Test03();
new Thread(()->test03.m1()).start();
}
}
5.2、重入锁第二种,子类同步方法调用父类同步方法。
public class Test05 {
public synchronized void m() throws InterruptedException {
System.out.println("m start");
TimeUnit.SECONDS.sleep(1);
System.out.println("m end");
}
public static void main(String[] args) throws InterruptedException {
new TT().m();
}
}
class TT extends Test05 {
@Override
public synchronized void m() throws InterruptedException {
System.out.println("child m start");
super.m();
System.out.println("child m end");
}
}
6、死锁
方法m1锁定对象o1的过程中去锁定o2, m2锁定o2的过程中去锁定o1,两个线程相互等待,都无法获取o2, o1,造成死锁。
public class DeadLock {
private Object o1 = new Object();
private Object o2 = new Object();
public void m1() throws InterruptedException {
synchronized (o1) {
System.out.println("m1--o1被锁定");
Thread.sleep(5000);
synchronized (o2) {
System.out.println("m1--o2被锁定");
Thread.sleep(5000);
}
}
}
public void m2() throws InterruptedException {
synchronized (o2) {
System.out.println("m2--o2被锁定");
Thread.sleep(1000);
synchronized (o1) {
System.out.println("m2--o1被锁定");
Thread.sleep(5000);
}
}
}
public static void main(String[] args) throws Exception {
DeadLock deadLock = new DeadLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
deadLock.m1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
new Thread(new Runnable() {
@Override
public void run() {
try {
deadLock.m2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
线程之间互相通信的方法:(1)都去读共享内存;(2)互相发消息。
6.2、异常发生,锁会被释放
程序在执行过程中,如果出现异常,默认情况锁会被释放。所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常。
public class Test06 {
int count = 0;
public 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 n = 1/0;
}
}
}
public static void main(String[] args) {
Test06 test06 = new Test06();
Runnable r = new Runnable() {
@Override
public void run() {
test06.m();
}
};
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}
7、锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变,应该避免将锁定对象的引用变成另外的对象。
public class T {
Object o = new Object();
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) {
T t = new T();
//启动第一个线程
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建第二个线程
Thread t2 = new Thread(t::m, "t2");
//锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
t.o = new Object();
t2.start();
}
}
8、不要以字符串常量为锁定对象
在下面的例子中,m1和m2其实锁定的是同一个对象,这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁
public class T {
String s1 = "Hello";
String s2 = "Hello";
void m1() {
synchronized(s1) {
}
}
void m2() {
synchronized(s2) {
}
}
}
二、volatile关键字
JMM原理:每个线程执行过程中有自己的一块内存(内存,缓冲区等),执行过程中,每个线程把主内存的内容读过来在自己的内存中修改,此过程中不再去主内存中读取,直到完成之后写回到主内存。
程序理解:线程1开始执行,复制running到自己的内存,是true,while循环进入死循环。1ms后线程2开始执行,复制主内存的一个变量到自己的内存,修改,重新写回到主内存。如果running不加volatile,第一个线程不会再去主内存中读取running,一直在死循环,无法结束。如果running加了volatile,一旦这个值发生改变会通知别的线程,你们的内存中的数据过期了,请再重新读一下。读取之后running为false,线程1结束。
如果线程1处理过程中,有System.out.println或sleep操作,cpu可能会空闲的时候去主内存中读取数据。
volatile的作用:写完之后进行缓存过期通知,要保证线程之间的可见性,要对线程之间共同访问的变量加volatile。如果不加volatile只能加synchonized。能用volatile的时候就不用synchonized,程序的性能提高很多。
volatile和synchonized区别:
volatile只能保证可见性。对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
synchonized既能保证可见性也能保证原子性。
public class T {
/*volatile*/ boolean running = true;
void m() {
System.out.println("m start");
while(running) {
}
System.out.println("m end!");
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}
}
volatile写和volatile读的内存语义总结:
(1)volatile写:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
(2)volatile读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
(3)线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
(4)线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
(5)线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
三、AtomXXX类,用于简单的数字运算
AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用时的原子性。
public class T {
/*volatile*/ //int count = 0;
AtomicInteger count = new AtomicInteger(0);
/*synchronized*/ void m() {
for (int i = 0; i < 10000; i++)
// 加了if语句,就不能保证原子性,尽管get()和incrementAndGet()都是原子方法
// if(i < 1000) count.get()
count.incrementAndGet();
}
public static void main(String[] args) {
T t = new T();
List<Thread> threads = new ArrayList<Thread>();
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);
}
}
四、示例程序
要求:实现一个容器,提供两个方法,add,size。写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束。
4.1、用volatile修饰list,一旦list发生改变会通知其他线程,线程t2可以监控list的变化,在size==5时发出通知。
public class MyContainer {
// 用volatile修饰list,一旦list发生改变会通知其他线程,线程t2可以监控list的变化,在size==5是发出通知。
private volatile List list = new ArrayList();
public void add(Object o) {
list.add(o);
}
public int size() {
return list.size();
}
public static void main(String[] args) throws InterruptedException {
MyContainer container = new MyContainer();
new Thread(()-> {
for(int i=0; i<10; i++) {
container.add(new Object());
System.out.println("container.size:" + container.size());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
new Thread(()-> {
while (true) {
if(container.size() == 5) {
break;
}
}
System.out.println("t2 结束");
}, "t2").start();
}
}
缺点:(1)没加同步,container.size() == 5时,有可能在break之前有其他线程进入,不精确。
(2)t2的死循环浪费cpu
4.2、wait会释放锁,notify不会释放锁
wait,notify必须锁定,不锁定就不能调用对象的wait,notify方法
问:为什么t1 notify之后,还要wait?
答:notify不会释放锁,即使notify了t2也不会执行,需要调用wait,才会释放锁,让t2执行。t2执行结束,调用notify,t1会继续执行。
注意:运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以
public class MyContainer3 {
private volatile List list = new ArrayList();
public void add(Object o) {
list.add(o);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
MyContainer3 myContainer3 = new MyContainer3();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (myContainer3) {
System.out.println("t2开始");
if(myContainer3.size() != 5) {
try {
myContainer3.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
myContainer3.notify();
}
System.out.println("t2结束");
}
}
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
synchronized (myContainer3) {
System.out.println("t1开始");
for(int i=0; i<10; i++) {
myContainer3.add(new Object());
System.out.println("myContainer3.size:" + myContainer3.size());
if(myContainer3.size() == 5) {
myContainer3.notify();
// notify不会释放锁,即使notify了t2也不会执行,
// 需要调用wait,才会释放锁,让t2执行
try {
myContainer3.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}, "t1").start();
}
}
4.3、使用Latch(门闩)替代wait notify来进行通知
好处是通信方式简单,同时也可以指定等待时间。
countDownLatch.countDown(); 1变成0,门闩就开了,其他线程就可以执行了。
使用await和countdown方法替代wait和notify,CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行
当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了,这时应该考虑countdownlatch/cyclicbarrier/semaphore
public class MyContainer4 {
private volatile List list = new ArrayList();
public void add(Object o) {
list.add(o);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
MyContainer4 myContainer4 = new MyContainer4();
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t2开始");
if(myContainer4.size() != 5) {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2结束");
}
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1开始");
for(int i=0; i<10; i++) {
myContainer4.add(new Object());
System.out.println("myContainer4.size:" + myContainer4.size());
if(myContainer4.size() == 5) {
countDownLatch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}