高并发编程学习笔记一:
Java高并发编程主要有三个知识点:
1. synchronizer:同步器,确保各线程之间通讯,同步以保证动作一致
2. 同步容器
3. ThreadPool、executor、callable等
1.Synchronized关键字
public class Demo {
public int count = 10;
Object o = new Object();
public void m(){
synchronized(o){//线程运行到此时,必须先拿到o这把锁
count--;
}
}
}
1. 当有第一个线程获取到了锁o时,其他线程要等待其结束,等待当前线程释放锁之后再获取锁。互斥锁
2. synchronized锁定的是o这个对象的堆内存地址,所以如果对象o发生改变,锁也会发生改变。
3. synchronized不同用法:
1.sychronized(this):以当前对象为锁,常用来锁定代码块
2.public synchronized void m(){} :锁定整个方法
4. public sychronized static void m(): 当synchronized锁定static方法时,因为static方法是和类绑定的,与对象无关所以相当于synchronized(ClassName.Class)锁定的是class对象。
2.脏读问题:只对set方法锁定没有对get方法锁定,导致当一个线程修改了数据但是还没有立马更新到数据库中,另一个线程获取该数据那么获取到的就是无效数据。所以解决方法是:set与get均加锁,写的时候不能读,读的时候不能写。
public class Demo1 {
String accountName;
Double balance = 0.00;
public synchronized void set(String accountName,double balance){
this.accountName = accountName;
try {
Thread.sleep(2000);//模拟数据修改但是还没有更新到数据库中的状态
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
public /*synchronized*/ void get(String accountName){
System.out.println("The balance of "+accountName+" is "+this.balance);
}
public static void main(String[] args){
Demo1 demo1 = new Demo1();
new Thread(()->demo1.set("zhang san",100.00)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo1.get("zhang san");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo1.get("zhang san");
}
}
result:
The balance of zhang san is 100.0
The balance of zhang san is 100.0
3.synchronized的重入性
//sychronized重入
public class Demo2 {
public synchronized static void m1(){
System.out.println("m1 start");
m2();
new Demo2().new test().m3();
System.out.println("m1 end");
}
synchronized static void m2(){
System.out.println("m2 start");
System.out.println("m2 end");
}
class test{
void m3(){
synchronized (Demo2.class){
System.out.println("m3 start");
}
}
}
public static void main(String[] args){
Demo2 demo2 = new Demo2();
new Thread(()->demo2.m1()).start();
}
}
1.一个同步方法可以调用另一个同步方法。
2.线程只要获得了锁就可以调用以该锁为锁的所有方法(可以是不同类的方法,如上面代码所示m1调用m3),所以synchronized是可以重入的。
4. synchronized的重入性之子类调用父类方法
public class Demo1 {
synchronized void m(){
System.out.println("super start");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("super end");
}
public static void main(String[] args){
new subClass().n();
}
}
class subClass extends Demo1{
synchronized void n(){
System.out.println("child start");
super.m();
System.out.println("child end");
}
}
5. 当同步方法种抛出异常的时候,synchronized的锁会被释放。被其他的线程获取
public class Demo2 {
synchronized void m(){
int index = 0;
System.out.println(Thread.currentThread().getName()+ "start");
while (true){
index++;
if(index == 5){ //如果用try catch来handle这个异常 就不会报错 不会释放锁
index/=0;
}
}
}
public static void main(String[] args){
Demo2 demo2 = new Demo2();
new Thread(()->demo2.m()).start();
new Thread(()->demo2.m()).start();
}
}
result:
Thread-0 start;
ArithmeticException
Thread-1 start;
6. volatile关键字
Java程序在运行时会把需要的参数从主内存copy到自己的内存缓冲区,当调用参数的时候,不再再次请求主内存的参数,而是直接用自己内存缓冲区的参数。所以当多线程运行的时候,假如线程1修改了某一参数的值,线程2并不知道该参数被修改,因为它还是调用自己内存缓冲区中的该参数的值,这样会造成麻烦。
1. volatile 修饰的参数是线程间透明的,当volatile修饰的参数修改时,其他的线程也会被通知自己内存缓冲区的该参数过期,其他线程会再次去主内存中请求该参数。
2. volatile 修饰的方法禁止虚拟机对命令重排:为了代码效率,JVM会对程序代码重排执行,加了volatile后代码不会被重排。
3. volatile 只解决了可见性问题并没有解决原子性问题,所以volatile并不能取代synchronized。java.util.concurrent.aromicXXX包可以创建原子性的参数。
public class Demo4 {
volatile int count = 0;
private void add(){
count++;
}
public static void main(String[] args){
List<Thread> threads = new ArrayList<>();
Demo4 demo4 = new Demo4();
for(int i=0;i<10;i++){
threads.add(new Thread(()->{
for (int j=0;j<1000;j++){
demo4.add();
}
}));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("count : "+demo4.count);
}
}
result:8357
由此可见:volatile只能解决可见性问题,并不能解决原子性问题。所以要想解决原子性问题只能用 synchronized锁定add方法或者用java.util.concurrent.aromicXXX包可以创建原子性的参数。
1. 使用synchronized:
int count = 0;
private synchronized void add(){
count++;
}
2. 使用AtomicXXX
AtomicInteger count = new AtomicInteger(0);
private synchronized void add(){
count.getAndAdd(1);
}
7. 例题 : 实现一个容器,提供两个方法add和size。写两个线程:线程一将1-10是个元素加入到容器中,线程二监测容器的size,当size=5时,线程二给出提示并结束 简单的用volatile实现:在list上面加上volatile使第二个线程能够得知list的变化。
缺点:第二个线程死循环极大的浪费CPU
public class Demo3 {
volatile List container = new ArrayList();
private int size(){
return container.size();
}
private void add(Object o){
container.add(o);
}
public static void main(String [] args){
Demo3 demo3 = new Demo3();
new Thread(()->{
for(int i = 0;i<10;i++){
demo3.add(i);
System.out.println("t1 size : " + demo3.size());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
while (true){
if(demo3.size() == 5){
break;
}
}
System.out.println("t2 finish");
}).start();
}
}
使用wait与notify实现:
1. 当线程调用自己锁定对象的wait方法时,当前线程会进入wait状态,并释放自己的锁定对象。当调用notify方法时,会随机唤醒以这个对象为锁定对象并且处于wait状态的线程。notifyAll就是全部唤醒wait的线程。
2. notify并不会释放锁(以下代码有一个问题:lock.notify()并不会释放锁但是好像也能实现功能。那是因为for里面的if判断导致lock.notify其实是被运行了5次,等t1结束了,锁自动释放了t2才运行,与我们的设计不符)
public class Demo3 {
List container = new ArrayList();
private int size(){
return container.size();
}
private void add(Object o){
container.add(o);
}
public static void main(String [] args) {
Demo3 demo3 = new Demo3();
Object lock = new Object();
new Thread(() -> {
synchronized (lock){
System.out.println("t2 start");
if(demo3.size() != 5){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 finish container size is : " + demo3.size());
}
},"t2").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
if (demo3.size() != 5) {
demo3.add(i);
System.out.println("t1 size : " + demo3.size());
} else {
lock.notify();
}
}
}
},"t1").start();
}
}
//更新后更符合逻辑的代码
缺点:第二个线程运行的时候,第一个线程无法同步运行只能等待。
public class Demo3 {
List container = new ArrayList();
private int size(){
return container.size();
}
private void add(Object o){
container.add(o);
}
public static void main(String [] args) {
Demo3 demo3 = new Demo3();
Object lock = new Object();
new Thread(() -> {
synchronized (lock){
System.out.println("t2 start");
if(demo3.size() != 5){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 finish container size is : " + demo3.size());
}
},"t2").start();
new Thread(() -> {
System.out.println("t1 start");
for (int i = 0; i < 10; i++) {
synchronized (lock) {
demo3.add(i);
System.out.println("t1 size : " + demo3.size());
if(demo3.size() == 5){
//唤醒t2
lock.notify();
try {
//释放lock锁
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
System.out.println("t1 finish ");
},"t1").start();
}
}
使用countdownlatch
1.CountDownLatch主要有两个方法:countDown 与 await ,countDownLatch在创建的时候需要提供一个整数,这个整数表示有几个门闩,也表示处于await状态的线程执行之前应该完成多少个任务,其他线程每完成一个任务就调用countdown让这个整数减一,拿掉一根门闩。当门闩=0的时候,处于await状态的线程开始运行。 适合多任务的环境下,一个任务分成几个线程去完成,提高工作效率。
public class Demo3 {
volatile List container = new ArrayList();
private int size(){
return container.size();
}
private void add(Object o){
container.add(o);
}
public static void main(String [] args) {
Demo3 demo3 = new Demo3();
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2 start");
if(demo3.size() != 5){
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 finish container size is : " + demo3.size());
},"t2").start();
new Thread(() -> {
System.out.println("t1 start");
for (int i = 0; i < 10; i++) {
demo3.add(i);
System.out.println("t1 size : " + demo3.size());
if(demo3.size() == 5){
countDownLatch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 finish ");
},"t1").start();
}
}