并发基础入门(一)

  • JUC概述:java.util.concurrent包的简称,以处理线程为主,自jdk1.5开始出现。

一.JUC概述和进程线程等基本概念

1.进程与线程

  • 进程:系统进行资源分配和调度的基本单位,是程序的实体
  • 线程:操作系统进行运算调度的最小单位,一条线程是指进程中一个单一顺序的控制流。
  • 两者关系示例:打开360安全卫士,即开启一个进程,使用它的功能模块,例如木马查杀,电脑清理等,即开启它的一个线程

2.线程状态

  • 线程状态枚举类Thread.State
    1.NEW(新建)
    2.RUNNABLE(准备就绪)
    3.BLOCKED(阻塞)
    4.WAITING(不见不散)
    5.TIMED_WAITING(过时不候)
    6.TERMINATED(终结)

3.wait()和sleep()

  • 区别一:sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用
  • 区别二:sleep不会释放锁,wait会释放锁
  • 区别三:调用wait()使线程状态变为WAITING;调用sleep()使线程状态变为TIMED_WAITING
  • 区别四:wait()必须放入同步块中,sleep()不需要(wait必须放入同步块原因链接:https://juejin.im/post/5e6a4d8a6fb9a07cd80f36d1)

4.串行和并行

  • 串行:按先后顺序执行任务,一次只能取到一个任务(一对一)
  • 并行:可以同时取到多个任务,并且可以同时执行这些任务(多对多)
  • 并发:多个线程同时访问同一资源(多对一)

5.管程

  • 概念:在系统层面称为Monitor(监视器),在java层面称为锁
  • 作用:作为一种同步机制,保证同一时间,只有一个线程访问被保护的数据或代码
  • 示例:jvm同步,即加锁和解锁,基于管程对象实现

6.用户线程和守护线程

  • 用户线程概念:自定义线程
  • 守护线程概念:运行在后台的线程,例如垃圾回收线程
  • 线程与jvm
    1.主线程(守护线程)结束,自定义线程还在运行,则jvm不会结束
    2.全部为守护线程,它们结束后,则jvm也会结束

二.线程间通信

1.多线程编程步骤

  • 第一步:创建资源类,在资源类创建属性和操作方法
class Ticket{
    int tickets = 0;
    public synchronized void incr(){
    if(tickets != 0){
         try {
                 this.wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     tickets++;
     System.out.println(Thread.currentThread().getName() + ":" + tickets);
     this.notifyAll();
    }

    public void decr() throws InterruptedException {
       if(tickets != 1){
         try {
                 this.wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
         tickets--;
         System.out.println(Thread.currentThread().getName() + ":" + tickets);
         this.notifyAll();
    }
}
  • 第二步:在资源类上操作方法
        Ticket ticket = new Ticket();
        new Thread(()->{
            for(int i = 0; i < 30; i++){
                try {
                    ticket.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"aa").start();
  • 第三步:创建多个线程,调用资源类的操作方法
  • 第四步:防止虚假唤醒问题
    1.解决办法:wait方法放入while循环中
    2.理解虚假唤醒问题:重点在于wait方法在哪里等待就在哪里唤醒(下表第四行C的操作),下表基于以上示例演示ABCD四个线程的虚假唤醒现象,经过第四行就会得到2的结果
A(+1)B(-1)C(+1)D(-1)
执行后唤醒其他线程阻塞阻塞阻塞
阻塞阻塞等待并释放锁阻塞
等待并释放锁阻塞等待阻塞
等待阻塞获得锁继续之前的操作阻塞

2.线程间定制通信(线程按指定顺序执行)

  • 方法:给每个线程增加不同的标志位
  • 示例
//资源类添加标志位flag,
class Ticket{
    private Lock lock = new ReentrantLock();
    //创建三个线程的Condition,用于指定唤醒哪一个线程
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    private int flag = 1;
    //以下为其中一个线程要执行的方法,注意用的是c1等待,
    //c2唤醒,即执行线程1后执行线程2,从而保证了线程执行的顺序
    public void print2(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 1){
                c1.await();
            }
            for(int i = 0; i < 2; i++){
                System.out.println(Thread.currentThread().getName() + ":" + i + ";轮数:" + loop);
            }
            flag = 2;
            c2.signal();
        }finally {
            lock.unlock();
        }
    }

三.集合线程安全问题

1.ArrayList线程不安全

  • 源码分析:通过如下方法,没有看到用了synchronized等线程安全处理措施,故判断为线程不安全
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
  • 示例证明:以下示例运行报错为Exception in thread "Thread-8" Exception in thread "Thread-13" Exception in thread "Thread-19" java.util.ConcurrentModificationException
List<String> list = new ArrayList<>();
        for(int i = 0; i < 20; i++){
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }).start();
        }
  • 解决办法一:使用Vector类
    源码分析:如下图,明显看到synchronized保证了线程安全
public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
  • 解决办法二:使用Collections类
    源码分析:以下源码可知,SynchronizedList的构造方法虽然没有用synchronized等线程安全措施,但其add()方法加入了锁mutex
public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }
SynchronizedList(List<E> list) {
            super(list);
            this.list = list;
        }
public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }
        
  • 解决办法三:使用CopyOnWriteArrayList
    原理:写时复制,即当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
    源码分析:
public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        //加锁
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //复制容器元素并扩容
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //添加新元素
            newElements[len] = e;
            //新容器覆盖原来容器成为该类的数组
            setArray(newElements);
            return true;
        } finally {
        //解锁
            lock.unlock();
        }
    }

2.HashSet线程安全问题

  • 源码分析:以下代码深入到底层putVal都没有发现synchronized等线程安全措施,故判断为线程不安全
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

  • 示例证明:
Set<String> set = new HashSet<>();
        for(int i = 0; i < 20; i++){
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(set);
            }).start();
        }

Exception in thread "Thread-7" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)
	at java.util.HashMap$KeyIterator.next(HashMap.java:1469)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at demo.test.lambda$main$0(test.java:19)
	at java.lang.Thread.run(Thread.java:748)
//
  • 解决办法:使用CopyOnWriteArraySet(以下源码分析同CopyOnWriteArrayList)
public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

3.HashMap线程安全问题

  • 源码分析:没有synchronized等线程安全措施,判断线程不安全
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

  • 示例证明
Map<String, String> map = new HashMap<>();
        for(int i = 0; i < 20; i++){
            String s = String.valueOf(i);
            new Thread(()->{
                map.put(s, UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }).start();
        }

Exception in thread "Thread-8" Exception in thread "Thread-15" Exception in thread "Thread-13" Exception in thread "Thread-17" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1479)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1477)
	at java.util.AbstractMap.toString(AbstractMap.java:554)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at demo.test.lambda$main$0(test.java:21)
	at java.lang.Thread.run(Thread.java:748)
  • 解决方法:使用ConcurrentHashMap

四.synchronized锁的八种情况

1.第一类–执行两个被synchronized修饰的方法

class Phone{
    public synchronized void process1(){
        System.out.println("process1");
    }

    public synchronized void process2(){
        System.out.println("process2");
    }

    public void process3(){
        System.out.println("process3");
    }
}

  • 执行同一对象的两个不同synchronized方法,执行顺序按代码先后顺序执行
Phone phone = new Phone();
       //先执行
        new Thread(()->{
            phone.process1();
        },"aa").start();
       //后执行
        new Thread(()->{
            phone.process2();
        },"bb").start();

//答案如下:
process1
process2
  • 执行同一对象的两个不同synchronized方法,并且其中一个方法延迟睡眠了,执行顺序按代码先后顺序执行
//修改代码如下:
public synchronized void process1() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("process1");
    }
//答案如下:
process1
process2
  • 结论:一个类中用synchronized修饰的方法,在执行时锁住的是当前类的对象,如果同时调用多个这样的方法,由于都是同一对象的方法,则只能按代码从上到下执行顺序执行这些方法,而不会出现顺序错乱的情况。

2.第二类–执行一个synchronized修饰的方法和普通方法

  • synchronized修饰的方法延迟睡眠,另一个普通方法正常执行
Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.process1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"aa").start();

        new Thread(()->{
            phone.process3();
        },"bb").start();
//答案如下:
process3
process1
  • 分析:虽然普通方法process3没有保证线程安全,但方法process1在保证线程安全的情况下会延迟睡眠,就会轮到process3

3.第三类–两个实例对象执行不同的synchronized修饰的方法

Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(()->{
            try {
                //第一个对象phone执行process1方法
                phone.process1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"aa").start();

        new Thread(()->{
            //第二个对象phone1执行process2方法
            phone1.process2();
        },"bb").start();
//答案如下
process2
process1
  • 分析:由于process1方法有延迟睡眠,而且process1和process2用到不是同一把锁,则先执行process2方法

4.第四类–两个静态同步方法,执行对象为同一个或两个

  • 执行对象为同一个,结果先输出process1,后输出process2
public static synchronized void process1() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("process1");
    }

    public static synchronized void process2(){
        System.out.println("process2");
    }
public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.process1();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"aa").start();

        new Thread(()->{
            try {
                phone.process2();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"bb").start();
    }
    
  • 执行对象为两个不同的对象,先输出process1,后输出process2
public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(()->{
            try {
                phone.process1();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"aa").start();

        new Thread(()->{
            try {
                phone1.process2();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"bb").start();
    }
  • 分析:对于静态同步方法,锁的是该类的字节码Class对象。一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
    而不管是同一个实例对象的静态同步方法之间,
    还是不同的实例对象的静态同步方法之间,只要它们是同一个类的实例对象!

5.第五类–执行一个静态同步方法和一个非静态同步方法

  • 执行对象为同一个实例,同时输出process1和process2
public static synchronized void process1() throws Exception {
        //TimeUnit.SECONDS.sleep(4);
        System.out.println("process1");
    }

    public synchronized void process2() throws Exception{
        System.out.println("process2");
    }

public static void main(String[] args) {
        Phone phone = new Phone();
        //Phone phone1 = new Phone();
        new Thread(()->{
            try {
                phone.process1();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"aa").start();

        new Thread(()->{
            try {
                phone.process2();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"bb").start();
    }
  • 执行对象为两个不同实例,同时输出process1和process2
public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(()->{
            try {
                phone.process1();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"aa").start();

        new Thread(()->{
            try {
                phone1.process2();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"bb").start();
    }
  • 分析:静态同步方法加的锁是类锁,普通同步方法加的是对象锁,两个锁是不同的,所以自然不存在竞争,并行执行的,不存在先后顺序
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值