1 多线程并发

1.1 synchronized内部实现–升级锁
  1. 第一次线程访问,先为Object对象加偏向锁(只记录线程id),以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁
  2. 如果新线程不是原来的线程,偏向锁升级为自旋锁(类似于一个while循环等待,一般循环10次)
  3. 如果还得到这个锁,升级为重量级锁(操作系统中的锁,即os锁)
  4. os锁不占cpu,自旋锁占cpu
  5. 加锁部分代码执行时间长,线程数多,用系统锁os,执行时间短,且线程少,使用自旋锁
  6. Lock实际上是CAS,类似于自旋锁的逻辑,占cpu
1.2 线程间的不可见性
  1. 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本,他们从主内存中拷贝得到
  2. 线程对变量的副本的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
  3. 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量的传递需要通过主内存来完成
  4. 工作内存中变量值改变,会向主内存刷新,而主内存改变后,当子线程被cpu调度,从就绪转为运行时,主内存会将最新的变量值,重新拷贝给子线程,但这两个动作都有一定的延时(其实非常短)
  5. 线程A的工作内存A1修改变量 值,如果该值向主内存刷新数据前,线程B被调度,那么此时主内存值还未改变,因此给B拷贝的变量,还是未改变前的值,因此产生问题
package listen;

import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

public class ThreadUnSee implements Runnable {
    volatile int i = 0;
    public static void main(String[] args) {
        ThreadUnSee threadUnSee = new ThreadUnSee();
        new Thread(threadUnSee).start();
        for(int a=0;a<1000000;a++){
            System.out.println(threadUnSee.i+"--"+a);
        }
    }
    //无论是否使用volatile修饰,上面显示打印0,然后过了两秒钟后,run方法修改了i的值,马上返给主方法,此时打印100,再过两秒钟后,run再次修改i,然后又马上返给主方法,主方法打印0
    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        i=100;
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        i=0;
    }
}

1.3 volatile与synchronized
  1. 对变量加volatile对比不加volatile,只能让线程更及时的获取变量的最新值,而不是不加volatile就获取不到变量的最新值
  2. volatile保证线程可见性:只能修饰成员变量。volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。
    1. 线程可能运行在不同cpu上,一个cpu上值改了,另一个cpu不一定能马上看到,因此保证线程可见性是通过cpu的缓存一致性协议实现的(mesi)
  3. synchronized保证线程可见性:线程加锁时,必须重新从主内存中读取共享变量最新的值,线程解锁时,必须把共享变量的最新值刷新到主内存中
  4. 也就是说volatile与synchronized都可以保证线程间的可见性
  5. 但synchronized无法禁止指令重排序
//无volatile修饰时,只能打印m start,无法打印m end
//使用volatile后,两句话都可以正常打印
package listen;

import java.util.concurrent.TimeUnit;

public class T {
    volatile boolean running = true;
    void m(){
        System.out.println("m start");
        while(running){
			//此处一点代码没有,线程无法被重新调起,也就失去了被主内存重新推送新值的机会
			//一旦添加System.out.println(a);代码,同时在主线程中写入循环修改running值,即使running不设置为volatile,也一定会获取主内存中的值,并成功结束循环
			//没有volatile修饰,只不过结束的稍微晚一些
        }
        System.out.println("m end");
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m,"123").start();
        try{
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running=false;
    }
}

  1. 禁止指令重排序
package listen;

public class Mgr06 {
    private static /*volatile*/ Mgr06 INSTANCE;

    private Mgr06() {

    }
    //1. 单例的双重检查
    public static Mgr06 getINSTANCE() {
        //2. 为提高效率,不需同步的业务逻辑单独提炼出来
        if (INSTANCE == null) {
            //3. 不允许同时有两个线程访问,防止多线程导致单例失效
            synchronized (Mgr06.class) {
                //4. 两个线程可能同时访问到上方的==null代码,此时都是true,那么都会进入代码块,最后还是会创建出多个实例
                //5. 为避免这种情况,再次判断INSTANCE是否为null
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //6. 但如果定义INSTANCE时,不加volatile,还是有可能导致问题产生
                    //7. volatile功能:禁止指令重排序
                    //8. 正常情况,应先为Mgr06对象先分配内存,然后为Mgr06中非静态成员变量赋初值,最后将栈中的INSTANCE引用,指向新创建出的这个对象
                    //8. 但超高并发时,cpu为了加快速度,可能对指令重新排序,先为Mgr06对象分配内存,然后直接将栈中的INSTANCE引用,指向新创建出的这个对象,此时Mgr06对象的非静态成员变量还没赋初值
                    //10. 此时如果正好第二个线程进来,判断INSTANCE并不是null,就直接将这个非静态成员还没正确初始化的对象拿到手
                    //11. 加上volatile,该对象上的指令重排序不再允许存在,一定会先初始化,再指向栈中变量
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(Mgr06.getINSTANCE().hashCode())).start();
        }
    }
}

  1. volatile不能保证原子性,即不能替代synchronized
package listen;

import java.util.ArrayList;
import java.util.List;

public class VolatileRepSyn {
    volatile int count = 0;

    void m() {
        for (int i = 0; i < 10000; i++) count++;
    }

    public static void main(String[] args) {
        VolatileRepSyn volatileRepSyn = new VolatileRepSyn();
        List<Thread> threads = new ArrayList<>();
        for(int i=0;i<10;i++){
            //虽然定义了count为volatile,子线程一旦修改该值,就返回给主空间,而主空间又将返回后的值,通知给其他子线程
            //但有可能出现同时将count值为100通知给了10个线程,他们都自加一次后,都将101写回,这样,虽然在10个线程中,count值都增加了1,但总值也才增加1
            //必须使用synchronized void m(),来保证原子性
            threads.add(new Thread(volatileRepSyn::m,"thread-"+i));
        }
        threads.forEach((o)->o.start());
        threads.forEach((o)->{
            try{
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(volatileRepSyn.count);
    }
}

1.4 synchronized优化
  1. 锁的细化:同步代码块中的语句越少越好
  2. 锁的粗化:锁太多了,不如放到一起作为一个大锁。比如对一个表所有行都加锁,不如直接对表加锁
1.5 锁定对象属性改变
  1. 锁定的对象o,如果其属性改变,不影响锁的使用
  2. 但如果o变成另一个对象,就会影响。因为锁是对象头上的两位代表的,换对象后,对象头不一致
  3. 因此一般需要锁住对象,都用final定义
1.6 CAS(无锁优化、自旋)
  1. 所有Atomic开头的类,都是通过UnSafe类中的CAS操作实现的,是可以保证线程安全的类
  2. Unsafe类可以直接操作java虚拟机中的内存,相当于C或C++中的指针
    1. 分配内存:C语言:malloc,C++:new,UnSafe:allocateMemory
    2. 释放内存:C语言:free,C++:delete,UnSafe:freeMemory
package listen;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class T01_AtomicInteger {
    /*volatile int count = 0;*/
    //替代了synchronized,自身带有原子性
    AtomicInteger count = new AtomicInteger(0);
    /*synchronized*/ void m() {
        for (int i = 0; i < 10000; i++) count.incrementAndGet();
    }

    public static void main(String[] args) {
        T01_AtomicInteger t = new T01_AtomicInteger();
        List<Thread> 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.get());
    }
}
  1. CAS:compare and set,是一种有名的无锁(lock-free)算法。是CPU指令级操作,避免了请求操作系统来裁定锁的问题,只有一步原子操作,所以非常快
//cas算法伪代码
cas(原值,期望值,新值){
	//表示cas操作之前,该值未被其他进程修改过
	if(原值==期望值){
		//无需担心CAS方法内部执行时,变量值被其他线程修改,因为CAS是CPU指令级操作,CPU保证其内不可被修改
		原值==新值;
	}else{
		修改期望值,重新尝试
	}
}
1.7 AtomicStampedReference解决ABA问题
  1. ABA问题:是使用CAS方法修改值时,所产生的问题,我们假设,饭店希望判断所有账户余额小于30元的账户,由饭店自动帮其冲值20元,此时启了200个线程进行处理,每个线程都发现了账户A,其余额为25,那么第一个线程发现该账户时,使用cas操作,cas(25,25,45),此时如果剩下线程进入,由于原值已被修改,本次操作相当于cas(45,25,45),由于原值!=期望值,其他线程充值会失败,但如果此时恰好有客户来消费,消费了20元,那么余额又变为了25元,此时,如果还有剩余进程进来,由于余额被改回了25,相当于又是cas(25,25,45),因此充值操作再次被执行,产生问题
  2. ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态
  3. AtomicStampedReference在cas的基础上增加了一个标记stamp,类似一个版本号,标记被改了几次
1.8 LongAdder
  1. 使用了分段锁的概念,分段锁也使用了CAS算法,它将所有线程分为几部分,分别对应不同的几个锁对象,最后将所有锁对象上的值加和得到最终结果

  2. 适用于线程数特别多的情况

  3. 可重入锁的概念:锁定一个对象后,这个线程,还可以对这个对象,再锁一次,如果不可重入,子类调用父类会有问题

1.9 Java中的几种锁的概念
  1. 悲观锁:假设一定会发生并发冲突,通过阻塞其他所有线程来保证数据的完整性。Synchronized
  2. 乐观锁:假设不会发生并发冲突,直接不加锁去完成某项更新,如果冲突就返回失败。ReentrantLock(CAS机制)
  3. 公平锁能:老的线程排队使用锁,新线程仍然排队使用锁。new ReentrantLock(true)
  4. 非公平锁:老的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。new ReentrantLock()
  5. 共享锁(读锁、S锁):对象A被线程AT1加共享锁后,该对象允许被其他线程再加共享锁,但不允许被其他线程加排他锁。待补充
    1. 没有锁时,有可能读到其他线程写了一半的内容,产生脏读
    2. 加悲观锁后,读写都要等待,效率太低
    3. 因此产生共享锁
  6. 排他锁(写锁、X锁):对象A被线程AT1加排他锁后,其他线程不允许为该对象再加任何锁。待补充
1.10 保证线程同步的工具类
  1. CountDownLatch:“倒数的门栓”,可以控制N个线程都结束后,这N个线程才能继续
package com.mashibing.juc.c_020;

import java.util.concurrent.CountDownLatch;

public class T06_TestCountDownLatch {
    public static void main(String[] args) {
        //CountDownLatch与join区别:更灵活,可以在一个线程中,就调用多次countDown方法,这样一个线程结束,就有可能使await阻塞结束
        usingJoin();
        usingCountDownLatch();
    }

    private static void usingCountDownLatch() {
        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                int result = 0;
                for (int j = 0; j < 10000; j++) result += j;
                //latch中计数减1
                latch.countDown();
            });
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }

        try {
            //一直等待latch中计数成0,否则一直阻塞
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("end latch");
    }

    private static void usingJoin() {
        Thread[] threads = new Thread[100];

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                int result = 0;
                for (int j = 0; j < 10000; j++) result += j;
            });
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }

        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("end join");
    }
}
  1. CyclicBarrier:“循环栅栏”,类似循环的人满发车,所谓"栅栏",就是人满了就推倒栅栏,可以通行,再满再推
package com.mashibing.juc.c_020;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class T07_TestCyclicBarrier {
    public static void main(String[] args) {
        //CyclicBarrier barrier = new CyclicBarrier(20);
        //1. parties表示满多少人发车,barrierAction表示人满后启动一个新的线程
        CyclicBarrier barrier = new CyclicBarrier(20, () -> System.out.println("发车"));
        //3. i为100时,打印5次发车,为99时,打印4次发车,同时最后剩余的19个线程,都等待一个新的线程进入
        for (int i = 0; i < 99; i++) {
            new Thread(() -> {
                try {
                    //2. 阻塞线程,直到人满,并且人数+1
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  1. Phaser:“多个栅栏”,可以指定穿过每个栅栏的条件
package com.mashibing.juc.c_020;

import java.util.Random;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;

public class T09_TestPhaser2 {
    static Random r = new Random();
    static MarriagePhaser phaser = new MarriagePhaser();


    static void milliSleep(int milli) {
        try {
            TimeUnit.MILLISECONDS.sleep(milli);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        //1. 设置初始阶段需要推倒栅栏的人数,也可以在new时就进行设置,该数值在上一个阶段结束后重置回到原值
        phaser.bulkRegister(7);

        for (int i = 0; i < 5; i++) {

            new Thread(new Person("p" + i)).start();
        }

        new Thread(new Person("新郎")).start();
        new Thread(new Person("新娘")).start();

    }


    static class MarriagePhaser extends Phaser {
        //2. 当栅栏推倒时,自动调用该方法,phase表示第几个阶段,从0开始,每次增加1,registeredParties表示该阶段参与人数
        //一旦该方法返回true,表示后面栅栏全部失效
        @Override
        protected boolean onAdvance(int phase, int registeredParties) {
            switch (phase) {
                case 0:
                    System.out.println("所有人到齐了!" + registeredParties);
                    System.out.println();
                    return false;
                case 1:
                    System.out.println("所有人吃完了!" + registeredParties);
                    System.out.println();
                    return false;
                case 2:
                    System.out.println("所有人离开了!" + registeredParties);
                    System.out.println();
                    return false;
                case 3:
                    System.out.println("婚礼结束!新郎新娘洞房!" + registeredParties);
                    return true;
                default:
                    return true;
            }
        }
    }


    static class Person implements Runnable {
        String name;

        public Person(String name) {
            this.name = name;
        }

        public void arrive() {

            milliSleep(r.nextInt(1000));
            System.out.printf("%s 到达现场!\n", name);
            //3. 先通知Phaser当前线程执行完毕,然后等待,直到本阶段需要的所有人到齐
            phaser.arriveAndAwaitAdvance();
            //4. arrive方法:只通知Phaser当前线程执行完毕,不等待
        }

        public void eat() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 吃完!\n", name);
            phaser.arriveAndAwaitAdvance();
        }

        public void leave() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 离开!\n", name);
            phaser.arriveAndAwaitAdvance();
        }

        private void hug() {
            if (name.equals("新郎") || name.equals("新娘")) {
                milliSleep(r.nextInt(1000));
                System.out.printf("%s 洞房!\n", name);
                phaser.arriveAndAwaitAdvance();
            } else {
                //5. 不阻塞,通知Phaser当前线程执行完毕,减少进入下一阶段总人数
                phaser.arriveAndDeregister();
                //6. 为当前阶段,增加一个参与者,该参与者在当前阶段相当于还没执行!
                //phaser.register()
            }
        }

        @Override
        public void run() {
            arrive();
            eat();
            leave();
            hug();
        }
    }
}
  1. ReadWriteLock:读(共享)写(排它)锁
package com.mashibing.juc.c_020;

import java.util.Random;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class T10_TestReadWriteLock {
    static Lock lock = new ReentrantLock();
    private static int value;

    static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static Lock readLock = readWriteLock.readLock();
    static Lock writeLock = readWriteLock.writeLock();

    public static void read(Lock lock) {
        try {
            lock.lock();
            Thread.sleep(1000);
            System.out.println("read over!");
            //模拟读取操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void write(Lock lock, int v) {
        try {
            lock.lock();
            Thread.sleep(1000);
            value = v;
            System.out.println("write over!");
            //模拟写操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        //使用ReentrantLock时,读、写都无法并发执行,效率特别低
        //Runnable readR = ()-> read(lock);
        Runnable readR = ()-> read(readLock);

//        Runnable writeR = ()->write(lock, new Random().nextInt());
        Runnable writeR = ()->write(writeLock, new Random().nextInt());
        //写时,加的是写锁(排它锁),即其他的读写都无法并行
        for(int i=0; i<2; i++) new Thread(writeR).start();
        //读时,加共享锁,读操作可以并行处理,写操作必须等待其前面执行的操作都结束
        for(int i=0; i<18; i++) new Thread(readR).start();



    }
}
  1. Semaphore:“信号灯”,灯亮可以执行,不亮不能执行,用于模拟限流
//类似8车道,只有2个收费站
package com.mashibing.juc.c_020;

import java.util.concurrent.Semaphore;

public class T11_TestSemaphore {
    public static void main(String[] args) {
        //Semaphore s = new Semaphore(2);
        //设置同时只允许2个线程执行
        Semaphore s = new Semaphore(2, true);

        new Thread(()->{
            try {
                //获取一次许可,允许的线程总数减少1。如果总许可数已为0,会阻塞
                s.acquire();

                System.out.println("T1 running...");
                Thread.sleep(2000);
                System.out.println("T1 running...");

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //允许的线程总数恢复1
                s.release();
            }
        }).start();

        new Thread(()->{
            try {
                s.acquire();

                System.out.println("T2 running...");
                Thread.sleep(2000);
                System.out.println("T2 running...");

                s.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
  1. Exchanger:“交换器”,两个线程交换数据用

package com.mashibing.juc.c_020;

import java.util.concurrent.Exchanger;

public class T12_TestExchanger {
	//Exchanger相当于一个容器,该容器可以存储两个值
	
    static Exchanger<String> exchanger = new Exchanger<>();

    public static void main(String[] args) {
        new Thread(() -> {
            String s = "T1";
            try {
            	//该方法阻塞,向容器放入一个值
                s = exchanger.exchange(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + s);
        }, "Thread-T1").start();

        new Thread(() -> {
            String s = "T2";
            try {
            	//当第二个线程,执行到exchange,也扔入一个值,然后容器交换两个值的位置,并通知两个线程继续向前走
            	//无法处理三个线程,交换两个线程的局部变量数据
                s = exchanger.exchange(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + s);
        }, "Thread-T2").start();
    }
}

  1. LockSupport:支持锁实现的类,可以替代synchronized+wait和notify的功能,notify无法叫醒指定线程,但LockSupport可以,注意wait只能由synchronized锁住的对象调用,await也是如此,必须在ReentrantLock锁住的代码块中,由Condition对象调用
package com.mashibing.juc.c_020;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class T13_TestLockSupport {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if(i == 5) {
                    //使线程阻塞
                    LockSupport.park();
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();

        try {
            TimeUnit.SECONDS.sleep(8);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after 8 senconds!");
        //叫醒指定的线程t。注意unpark可以优先于park执行,也不会报错
        LockSupport.unpark(t);
    }
}

1.11 练习
  1. 习题一:实现一个容器,提供两个方法,add和size。写两个线程,线程1向该容器中,从1开始,放10个数,线程监2控容器中元素个数,放入了5个时,线程2给出提示并结束
  2. 习题二:写一个固定容量同步容器,拥有put、get方法、getCount方法,最大容量为10,能够支持2个生产者线程以及10个消费者线程的阻塞调用
1.11.1 习题一
  1. 错误做法
package com.mashibing.juc.c_020_01_Interview;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;


public class T01_WithoutVolatile {

    List lists = new ArrayList();

    public void add(Object o) {
        lists.add(o);
    }

    public int size() {
        return lists.size();
    }

    public static void main(String[] args) {
        T01_WithoutVolatile c = new T01_WithoutVolatile();

        new Thread(() -> {
            for(int i=0; i<10; i++) {
                c.add(new Object());
                System.out.println("add " + i);

            }
        }, "t1").start();

        new Thread(() -> {
            while(true) {
            	//1. 不使用volatile时,可以加下面的打印,也能让t2结束,这是因为不加打印时,t2中使用c.size时候,不能特别及时看到t1中该值的改变,有可能t2跑到该段代码时,t2中的c.size还使用的之前的值4,然后就直接跳到了8,加打印可以增加t2中循环内的时间,增加了t1刷新缓存给主内存,而主内存又将新值分发给t2的机会
            	//2. 加上volatile后,能保证每次t2中的c.size都是最新值,但实际上,还有有可能,t1先执行,直接跑到了6,然后线程t2就永远无法结束循环,因此此处错误实际上并不是线程间是否可见导致的问题
                //System.out.println(Thread.currentThread().getName()+" "+c.size());
                //值得注意的是,volatile修饰的变量如果是对象或数,其含义是对象或数组的地址具有可见性,但是数组或对象内部的成员改变不具备可见性,但此处不能证明这点,因为volatile确实对size可见了,即使用volatile时,确实能做到让t2结束
                if(c.size() == 5) {
                    break;
                }
            }
            System.out.println("t2 结束");
        }, "t2").start();
    }
}
  1. object的notify方法不释放锁问题
package com.mashibing.juc.c_020;

import java.util.concurrent.TimeUnit;

public class TestSelf {
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (lock){
                System.out.println("t2启动");
                try {
                    //5. wait虽然释放了锁,但它下面想继续执行,必须再次拿到锁,一旦有其他线程持续占着锁,那么wait就无法继续执行
                    //注意不能使用sleep,sleep不释放锁,wait释放锁
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2结束");
                //4. 于此同时,由于t1还wait呢,必须由t2将其notify
//                lock.notify();
            }
        }).start();
        TimeUnit.SECONDS.sleep(5);
        new Thread(()->{
            synchronized (lock){
                System.out.println("t1启动");
                //1. 注意wait方法会释放锁,但notify不会,因此,一旦notify代码在加锁的代码块中,那么改段代码块必须执行完成,才能执行之前wait处代码
                //2. 也就是,此处没法直接叫醒线程t2,必须先将t1执行完成
                lock.notify();
                //3. 加入下方代码,利用wait再次释放锁,就能够保证t2先执行完成
//                try {
//                    lock.wait();
//                    TimeUnit.SECONDS.sleep(5);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
                System.out.println("t1结束");
            }
        }).start();
    }
}

  1. 使用LockSupport实现
package com.mashibing.juc.c_020_01_Interview;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class T07_LockSupport_WithoutSleep {

	// 添加volatile,使t2能够得到通知
	volatile List lists = new ArrayList();

	public void add(Object o) {
		lists.add(o);
	}

	public int size() {
		return lists.size();
	}
	static Thread t1 = null, t2 = null;
	public static void main(String[] args) {
		T07_LockSupport_WithoutSleep c = new T07_LockSupport_WithoutSleep();
		t1 = 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) {
					LockSupport.unpark(t2);
					LockSupport.park();
				}
			}
		}, "t1");
		t2 = new Thread(() -> {
			//System.out.println("t2启动");
			//if (c.size() != 5) {
				LockSupport.park();
			//}
			System.out.println("t2 结束");
			LockSupport.unpark(t1);
		}, "t2");
		t2.start();
		t1.start();
	}
}
1.11.2 习题二
  1. 所谓阻塞调用,就是当生产者为容器生产了10个对象后,不允许继续生产,应阻塞,等待消费者消费,而消费者消费完容器中所有的对象后,也应该阻塞,等待生产者继续生产
package com.mashibing.juc.c_020;

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyContainer2<T> {
    final private LinkedList<T> lists = new LinkedList<>();
    //容器中最多放十个元素
    final private int MAX = 10;
    //实时记录容器中元素个数
    private int count = 0;
    private Lock lock = new ReentrantLock();
    // ReentranLock的Condition的原理
    // 使用Condition时,相当于将等待队列变成了两个,一个producer,一个condition
    // Condition的本质是等待队列。producer.await();相当于让当前线程在producer队列中进行等待
    private Condition producer = lock.newCondition();
    private Condition consumer = lock.newCondition();

    public void put(T t) {
        try {
            //必须加锁,因为防止两个线程判断==MAX时,都成功,导致同时向容器中放入值,且++count,导致容器中元素超量,count也超过MAX
            lock.lock();
            //此处必须使用while,因为如果使用if,那么一个生产者线程A发现容器满了之后,就进入等待,当他被唤醒,就会向容器内放入值
            //但如果此时另一个成产者线程B,在他之前,已经向容器放入了元素,并导致容器再次变满,此时A并没有再次判断容器是否满,会的add方法放入第MAX+1个元素
            while (lists.size() == MAX) {
                producer.await();
            }
            lists.add(t);
            ++count;
            //Synchronized中锁对象的wait和notify,做不到只唤醒消费者,这是ReentrantLock与Synchronized的不同点
            consumer.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public T get() {
        T t = null;
        try {
            lock.lock();
            while (lists.size() == 0) {
                consumer.await();
            }
            t = lists.removeFirst();
            count--;
            producer.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return t;
    }

    public int getCount(){
        return count;
    }
    public static void main(String[] args) {
        MyContainer2<String> c = new MyContainer2<>();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 5; j++) {
                    System.out.println(c.get());
                    System.out.println("消费");
                }
            }, "c" + i).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 25; j++) {
                    c.put(Thread.currentThread().getName() + " " + j);
                }
            }, "p" + i).start();
        }
    }

}

1.12 源码阅读技巧
  1. 读源码很难,需要理解别人的思路
    1. 数据结构基础:java底层框架
    2. 设计模式:Mybatis、Spring
  2. 读源码时可以自己画泳道图,每个类占用一列,下面放它用到的方法,不同类的不同方法间用箭头连接,表示方法调用的顺序,Intellij上可以使用插件PlantUML File,下插件后,可以直接新建一个泳道图
  3. 然后可以画一个类之间的类图,Intellij插件可以直接生成,但最好自己画,可以是简单的:NonfairSync–>Sync–>AQS
  4. 源码阅读原则
    1. 跑不起来不读:可以debug跟进去,防止多态时,不知道具体进入哪个子类
    2. 解决问题就好:带着目的去读
    3. 一条线索到底:从一个方法入手去读
    4. 无关细节略过:例如边界值为什么+1
    5. 一般不读静态:
1.13 AQS简要源码分析
  1. AQS全称为AbstractQueuedSynchronizer,它提供了一个FIFO(先进先出)队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有ReentrantLock、CountDownLatch等
  2. AQS中使用一个volatile修饰共享变量state表示锁的状态,例如0表示无锁,1表示有锁,当一个线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node,使用CAS方法将Node节点加入到CLH队列尾部,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)
  3. 使用CAS操作对CLH队列尾部加锁,避免了对整个队列加锁,提高了效率
  4. 这个进先出队列:同时是一种CLH(Craig,Landin,and Hagersten)队列,即一个虚拟的双向队列
    1. 虚拟:这个双向队列仅存在节点之间的关联关系,而不存在队列实例
    2. 双向:每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点所以双向链表可以从任意一个节点开始很方便的访问前驱和后继
    3. AQS中有两个属性head和tail,会直接指向CLH队列的头部Node和尾部Node
  5. 抢占锁的方法acquireQueued:如果前置节点为头节点,当前节点就有资格去尝试挣这个锁,如果拿不到,就阻塞, 等前置节点叫醒
1.14 VarHandle

变量句柄对象,代表指向某个变量的引用,那么创建对象时就有引用,为什么又要用一个引用指向该对象。是因为使用VarHandle引用,允许对普通属性进行原子操作,即拥有compareAndSet方法,而且比反射更快,直接操纵二进制码

package com.mashibing.juc.c_021_03_VarHandle;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

public class T01_HelloVarHandle {

    int x = 8;

    private static VarHandle handle;

    static {
        try {
            //使用VarHandle引用指向x
            handle = MethodHandles.lookup().findVarHandle(T01_HelloVarHandle.class, "x", int.class);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        T01_HelloVarHandle t = new T01_HelloVarHandle();

        //plain read / write
        System.out.println((int)handle.get(t));
        handle.set(t,9);
        System.out.println(t.x);

        handle.compareAndSet(t, 9, 10);
        System.out.println(t.x);

        handle.getAndAdd(t, 10);
        System.out.println(t.x);

    }
}

1.15 java中的强软弱虚四种引用
  1. 强引用
package com.mashibing.juc.c_022_RefTypeAndThreadLocal;

public class M {
    //对象被垃圾回收后,会调用对象的finalize方法
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
    }
}
  1. 软引用
package com.mashibing.juc.c_022_RefTypeAndThreadLocal;

import java.lang.ref.SoftReference;

public class T02_SoftReference {
    public static void main(String[] args) {
        //建立一个引用m,它指向一个软引用对象,这个软引用对象内部有一个被GC特殊处理的引用,该引用遇到内存不足时的垃圾回收,会被置为null,其引用的对象也就被回收掉了,这个引用才指向byte[]对象,这个对象占10M内存
        //这种情况我们一般描述为建立一个软引用指向byte[]对象
        SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
        //这句话相当于将软引用对象都回收了,其内部的引用、以及引用所指向的byte[]对象自然也被回收
//        m = null;
		//get方法是获取软引用所指向的对象,此处为获取byte[]对象
        System.out.println(m.get());
        System.gc();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(m.get());
        //再分配一个数组,总内存从10M升到25M,heap将装不下,只有这种防止内存溢出的垃圾回收,会将软引用中的引用置为null,从而回收掉其指向的对象
        //修改启动时的JVM内存:Run--Edit Configurations--VM options-- -Xms20M -Xmx20M
        //软引用是用来描述一些还有用但并非必须的对象。一般用做缓存,取的时候从缓存取,一旦有新对象进来,就可以把老的回收掉
        byte[] b = new byte[1024*1024*15];
        System.out.println(m.get());
    }
}
  1. 弱引用:
package com.mashibing.juc.c_022_RefTypeAndThreadLocal;

import java.lang.ref.WeakReference;

public class T03_WeakReference {
    public static void main(String[] args) {
        //建立一个弱引用,指向M对象,弱引用一般用在容器中
        WeakReference<M> m = new WeakReference<>(new M());
        System.out.println(m.get());
        //弱引用遭到gc就会回收
        System.gc();
        System.out.println(m.get());
    }
}
  1. 虚引用:管理堆外内存
/**
 * 1. NIO中有一个DirectByteBuffer,直接内存,不被JVM直接管理,被操作系统管理,也叫做堆外内存
 * 2. 这种对象压根没在堆里,即使将该对象所有引用设为null,垃圾回收器没法回收他
 * 3. 可以使用虚引用指向堆外内存中的对象,当虚引用被垃圾回收器回收时,人为使用Unsafe类对堆外内存进行回收堆外内存,java11中的Unsafe无法直接访问了
 */

package com.mashibing.juc.c_022_RefTypeAndThreadLocal;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;

public class T04_PhantomReference {
    private static final List<Object> LIST = new LinkedList<>();
    private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();

    public static void main(String[] args) {
        //虚引用指向的对象,也是只要被gc就会被垃圾回收
        //指定虚引用所指向对象M,和垃圾回收后,将该对象放入的ReferenceQueue类型的队列QUEUE
        //弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用
        PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);
        new Thread(() -> {
            while (true) {
                //本例中需要将虚拟机内存调整为较小内存,方便触发垃圾回收
                LIST.add(new byte[1024 * 1024]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
                //虚引用永远也无法获取其引用的对象
                System.out.println(phantomReference.get());
            }
        }).start();

        new Thread(() -> {
            while (true) {
                Reference<? extends M> poll = QUEUE.poll();
                //一旦队列中能取出值了,就说明虚引用所指向对象被垃圾回收了,并放入了该队列
                if (poll != null) {
                    System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
                }
            }
        }).start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

1.16 弱引用的应用
  1. ThreadLocal
/**
* ThreadLocal作用:Spring的声明式事务中,帮助每个方法都可以拿到同一个数据库连接
* 声明式事务:是建立在AOP之上的。本质是对方法前后进行拦截,然后在目标方法开始之前创建或者* 加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是* 不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置* 文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用* 到业务逻辑中

* 为了保证一系列的方法是一个完整的事务,那么必须每个人拿到的对象,必须是同一个connection
* 对象,connection对象一般都在连接池中,为保证每次拿到的都是同一个connection对象,可以* 将第一次取到的connection对象放到当前线程的ThreadLocal中,以后再拿都直接从* 
* ThreadLocal拿,不从线程池中拿

 * ThreadLocal线程局部变量
 * 1. ThreadLocal的set方法,会向调用该set方法所在的线程T对应的t对象中的ThreadLocal.ThreadLocalMap类型(类似Map的结构)的成员变量threadLocals中的value设置值
 * 2. ThreadLocal.ThreadLocalMap的key,是调用该set方法的ThreadLocal对象,value是set方法后面的参数对象
 * 3. Thread.ThreadLocalMap
 * 4. 也就是说,对于同一个线程,同一个ThreadLocal对象,只能放一个Object值
 * 5. fcr为了对于同一个线程,同一个ThreadLocal放入多个值,就使用Map替换这个Object,这样就可以使用这个Map存放多组值,外表上看,也就是同一个线程,同一个ThreadLocal变量,可以放入多组值
 * 6. ThreadLocal是使用空间换时间,synchronized是使用时间换空间
 * 7. 比如在hibernate中session就存在与ThreadLocal中,避免synchronized的使用
 */
package com.mashibing.juc.c_022_RefTypeAndThreadLocal;

import java.util.concurrent.TimeUnit;

public class ThreadLocal2 {
    //volatile static Person p = new Person();
    static ThreadLocal<Person> tl = new ThreadLocal<>();

    public static void main(String[] args) {

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //注意ThreadLocal对象,实际上是有两个引用指向他的,一个是tl,一个是Thread中的ThreadLocalMap成员变量中的key
            //tl是强引用,而key继承了弱引用
            //1. Entry类继承弱引用的原因:可以方便的建立弱引用对象,让这个弱引用对象中的引用指向key对象,这样一来,当线程结束,tl就被指向null,这样就只有一个弱引用指向key对象,当再次垃圾回收,key对象就会被回收,从而防止内存泄露问题。如果不这样做,即使tl所在方法结束,由于key存在一个强引用指向,key对象永远不会被回收
            //注意内存泄露不是内存溢出,内存够大就永远不会溢出
            //2. 如果线程长期存在于线程池中,当key对象被回收,key值变为null,但其value由于有强引用指向,不会被回收,导致Map不会被回收,也导致ThreadLocal不会被回收,所以使用完ThreadLocal内存放的对象,必须remove掉
            System.out.println(tl.get());
        }).start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tl.set(new Person());
        }).start();
    }

    static class Person {
        String name = "zhangsan";
    }
}

你可能感兴趣的:(多线程与高并发编程)