Java高并发编程一
1、synchronized 关键字,锁住的不是一个代码块,而是一个对象;
2、一个synchronized代码块相当于一个原子操作,是不可分的;
3、同步和非同步方法是否可以同时调用? ----> 可以
4、对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题(dirtyRead)
5、一个同步方法是否可以调用另外一个同步方法? ----> 可以
一个线程已经拥有某个对象的锁,再次申请的时候是否仍然会得到该对象的锁? ----> 可以
也就是说synchronized获得的锁是可重入(即获得锁之后还可以再获取一遍锁)
6、继承中有可能发生的情形,子类重写父类的同步方法,然后再重写父类的同步方法中调用父类的同步方法,这是可以的,不会产生死锁
7、程序在执行过程中,如果出现异常,默认情况锁会被释放,所以在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
【线程抛出异常会释放锁,如果不想释放锁,可以添加try catch 进行异常处理】
比如:在一个web应用程序处理过程中,多个servlet线程共同访问同一资源,这时如果异常处理不合适,在第一个线程中抛出异常,
其它线程就会进入同步代码区,有可能会访问到异常产生时的数据,因此要小心处理同步业务逻辑中的异常。
8、使用volatile关键字,会让所有线程都会读到变量的修改值
【即当多个线程进行操作共享数据时,使用volatile关键字可以保证内存中的数据是可见的】
▲注意:
①、volatile 不具备互斥性;
②、volatile 不能保证数据的"原子性";
③、JVM的底层有一个优化,叫做重排序,使用volatile关键字后,不能重排序了
所以volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能代替synchronized
★ 在下面的代码中,running是存在于堆内存的t 对象中(堆内存、栈内存统称为主内存)
当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区中,在运行过程中,直接使用自己工作区中的值,
并不会每次都去读取堆内存中的running值,这样,当主线程修改running的值之后,t1线程感知不到,所以就不会停止运行
【注意】
在while中,即死循环中执行一些语句,比如添加sleep方法等的时候,可能会停止运行,因为可能某一时刻CPU空闲了,可能去会读取主内存中的running值
public class T {
/*volatile*/ boolean running = true; //对比有无volatile的情况下,整个程序的运行结果
void m() {
System.out.println("m start");
while(running) {
/*
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
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;
}
}
9、synchronized 可以保证可见性和原子性,volatile只能保证可见性
synchronized 的效率比volatile低很多
10、JDK1.5之后,在java.util.concurrent.atomic 包下提供了一些原子变量。
▲ 解决同样的问题的更高效的方法,就是使用atomicXXX类,atomicXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的;
▲ atomicXXX类可以保证可见性
11、synchronized优化:
同步代码块中的语句越少越好
public class T {
int count = 0;
synchronized void m1() {
//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();
}
}
}
12、锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成了另外一个对象,则锁定的对象会发生改变,
应该避免将锁定对象的引用变成另外的对象。
▲ 锁的是堆内存中真正new出来的对象上,而不是锁在栈内存的引用上
13、不要以字符串常量作为锁定对象,可能会出现死锁
在下面的例子中,m1和m2其实锁定的是同一个对象,这种情况还会发生比较诡异的现象,比如用到了一个类库,在该类库中代码锁定了字符串"hello",
但是你读不到源码,所以在自己的代码中也锁定了"hello",这时就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁。
public class T {
String s1 = "Hello";
String s2 = "Hello";
void m1() {
synchronized(s1) {
}
}
void m2() {
synchronized(s2) {
}
}
}
14、面试题:
实现一个容器,提供两个方法,add,size,写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
分析:
● 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做呢?
● 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁,需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以
● notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行,否则输出结果为:
输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出
public class T {
//添加volatile,使t2能够得到通知
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
T c = new T();
final Object lock = new Object();
new Thread(() -> {
synchronized(lock) {
System.out.println("t2启动");
if(c.size() != 5) { //和生产者消费者不同,此时不需要使用while
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
//通知t1继续执行
lock.notify();
}
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
synchronized(lock) {
for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);
if(c.size() == 5) {
lock.notify();
//释放锁,让t2得以执行
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}
上述方式整个通信过程比较繁琐
● 使用Latch(门闩)替代wait notify来进行通知,好处是通信方式简单,同时也可以指定等待时间
使用await和countdown方法替代wait和notify,CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行
当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了,这时应该考虑countdownlatch/cyclicbarrier/semaphore
public class T {
//添加volatile,使t2能够得到通知
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
T c = new T();
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2启动");
if(c.size() != 5) { //和生产者消费者不同,此时不需要使用while
try {
latch.await();
//也可以指定等待时间
//latch.await(5000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);
if(c.size() == 5) {
// 打开门闩,让t2得以执行
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}
网友评论