JUC——day02 &&集合的线程安全&& 多线程锁 &&callable接口

1.集合的线程安全问题

1.1 List

在多线程的环境下,同时向list中添加数据,会产生java.util.ConcurrentModificationException异常。

1.1.1 模拟多线程环境

public static void main(String[] args) {
    List<String> list = new ArrayList<>();

    for (int i = 0; i < 30; i++) {
        new Thread(()->{
            list.add(UUID.randomUUID().toString().substring(0,5));
            System.out.println(list);
        },String.valueOf(i)).start();
    }
}

在这里插入图片描述

1.1.2 原因

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

在源码中,对添加一个元素并没有进行加锁的操作,所以说ArrayList线程不安全。

1.1.3 解决方法

// 1. 使用线程安全类 Vector,add方法加锁
List<String> list = new Vector<>();

// 2. 使用 Collections 工具类封装 ArrayList,返回一个add方法加锁的list
List<String> list = Collections.synchronizedList(new ArrayList<>());

// 3. 使用 java.util.concurrent.CopyOnWriteArrayList;
List<String> list = new CopyOnWriteArrayList<>();

1.1.4 写时复制思想

CopyOnWrite 容器,写时复制,往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行Copy, 复制出一个新的容器Object[] newElements, 然后新的容器Object[] newElements 里添加元素,添加完元素之后,再将原容器的引用指向新的容器 setArray(newElements); 这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

//CopyOnWriteArrayList add()源码 ,写时加锁
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(); //解锁
    }
}

//CopyOnWriteArrayList indexOf()源码,读时共享
private static int indexOf(Object o, Object[] elements,
                           int index, int fence) {
    if (o == null) {
        for (int i = index; i < fence; i++)
            if (elements[i] == null)
                return i;
    } else {
        for (int i = index; i < fence; i++)
            if (o.equals(elements[i]))
                return i;
    }
    return -1;
}

1.2 Set

与 List 接口的测试方法相似,同样会抛出 java.util.ConcurrentModificationException 异常。

1.2.1 解决办法

// 1. 使用 Collections 工具类封装
Set<String> set = Collections.synchronizedSet(new HashSet<>());

// 2. 使用 java.util.concurrent.CopyOnWriteArraySet;
Set<String> set = new CopyOnWriteArraySet<>();

1.2.2 CopyOnWriteArraySet

// 底层实际上是一个 CopyOnWriteArrayList,所以的增删改其实都是调用CopyOnWriteArrayList的方法
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable {
    private static final long serialVersionUID = 5457747651344034263L;

    private final CopyOnWriteArrayList<E> al; 

    // ...
}
// 添加元素,相当于调用 CopyOnWriteArrayList 的 addIfAbsent() 方法
public class CopyOnWriteArraySet<E> {
    public boolean add(E e) {
        return al.addIfAbsent(e);
    }
}

/**
 * CopyOnWriteArrayList 的 addIfAbsent() 方法
 * Set 集合中的元素不可重复,如果原集合中有要添加的元素,则直接返回 false
 * 否则,将该元素加入集合中
 */
public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}

/**
 * 重载的 addIfAbsent() 方法,用于真正添加元素加锁后,再次获取集合,与刚才拿到的集合比较,
 * 两次拿到的不一样,说明集合被其他线程修改过了,重新比较最新集合中有没有该元素,如果比较
 * 后,没有返回 false,说明没有该元素,执行下面的添加方法。
 */
private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

1.3 Map

多线程环境下,会抛出 java.util.ConcurrentModificationException 异常。

1.3.1 解决方法

// 使用 Collections 工具类
Map<Integer, String> map = Collections.synchronizedMap(new HashMap<>());

// 使用 ConcurrentHashMap
Map<Integer, String> map = new ConcurrentHashMap<>();

1.3.2 HashMap,Hashtable和ConcurrentHashMap的区别

HashMapHashtableConcurrentHashMap
继承AbstractMapDictonary除了继承AbstractMap还实现了ConcurrentMap接口
线程安全不安全使用synchronized实现同步方法ConcurrentHashMap降低锁的粒度,对方法的代码块进行加锁(CAS+Synchronized),拥有更好的并发性能。
Key-Value值允许唯一的key为null,和任意个value为null不允许value和key为null不允许value和key为null
哈希算法不同使用 key 的 hashcode 值进行高16位和低16位异或再取模长度对 key 的hashcode值进行取模操作使用 key 的 hashcode 值进行高16位和低16位异或再取模长度
扩容机制不同扩容为原有数组长度的两倍,初始容量为16hashtable中的初始容量为11,容量为原有长度的两倍+1。扩容为原有数组长度的两倍,初始容量为16
失败机制支持快速失败支持快速失败支持安全失败
查询方法没有contains方法,但是拥有containsKey和containsValue方法还支持contains方法还支持contains方法
迭代方式不支持Enumeration迭代方式支持Enumeration迭代方式支持Enumeration迭代方式

2.多线程锁

对于一个锁住的资源,一个时刻只允许一个线程去进行访问。

2.1 锁对象执行顺序的探究

class Phone {

    public synchronized void sendSMS() throws Exception {
        //停留4秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

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

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


public class LockStudy {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(()->{
            try {
                phone1.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"AA").start();

        Thread.sleep(100);

        new Thread(()->{
            try {
                phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"BB").start();
    }
}

八种情况

1 标准访问,先打印短信还是邮件
------sendSMS
------sendEmail
2 停4秒在短信方法内,先打印短信还是邮件
------sendSMS
------sendEmail
3 新增普通的hello方法,是先打短信还是hello
------getHello
------sendSMS
4 现在有两部手机,先打印短信还是邮件
------sendEmail
------sendSMS
5 两个静态同步方法,1部手机,先打印短信还是邮件
------sendSMS
------sendEmail
6 两个静态同步方法,2部手机,先打印短信还是邮件
------sendSMS
------sendEmail
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
------sendEmail
------sendSMS
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
------sendEmail
------sendSMS

通过结果可以看到

  • synchronized锁的是方法,则是对象锁
  • 同个对象锁的机制要等待,不同对象锁的机制调用同一个不用等待
  • 加了static则为class锁而不是对象锁

所有的静态同步方法用的是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象

2.2 公平锁和非公平锁

2.2.1 定义

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

优点:所有的线程都能得到资源,不会饿死在队列中。

缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。


非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。

缺点:可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

在ReentrantLock中默认使用非公平锁,传入true使用公平锁

	/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
public ReentrantLock() {
    sync = new NonfairSync();
}

	/**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

2.2.2 公平锁

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();//获取状态位,不为0说明有线程正在占用这个资源
        if (c == 0) {
            //判断当前线程能不能获得锁,如果能够获得锁则返回false。如果队列为空,或是当前线程位于队列头部会返回false,其他情况返回true。
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);//设置当前持有锁的线程为自己
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { //当前锁被占用,此线程加入等待队列
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}


public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

2.2.3 非公平锁

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}


final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) { //直接尝试获取锁
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires; //失败加入等待队列
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

2.3 可重入锁

synchronized和lock都是可重入锁

  • synchronized是隐式锁,不用手工上锁与解锁,而lock为显示锁,需要手工上锁与解锁
  • 可重入锁也叫递归锁

实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。

如果同一个线程再次请求这个锁,计数器将递增;

每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。

关于父类和子类的锁的重入:子类覆写了父类的synchonized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁

Object o = new Object();
new Thread(()->{
    synchronized(o) {
        System.out.println(Thread.currentThread().getName()+" 外层");

        synchronized (o) {
            System.out.println(Thread.currentThread().getName()+" 中层");

            synchronized (o) {
                System.out.println(Thread.currentThread().getName()+" 内层");
            }
        }
    }

},"t1").start();

2.4 死锁

两个或以上的进程因为抢夺资源而造成互相等待资源的现象称为死锁

产生死锁的原因:

1.系统资源不足 2. 系统资源分配不当 3.进程运行顺序不当

验证是否是死锁

  1. jps 类似于linux中的ps -ef查看进程号 (jps-l)
  2. jstack 自带的堆栈跟踪工具 (jstack 进程号)

具体死锁的操作代码实例

public class DeadLock {

    //创建两个对象
    static Object a = new Object();
    static Object b = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (a) {
                System.out.println(Thread.currentThread().getName()+" 持有锁a,试图获取锁b");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println(Thread.currentThread().getName()+" 获取锁b");
                }
            }
        },"A").start();

        new Thread(()->{
            synchronized (b) {
                System.out.println(Thread.currentThread().getName()+" 持有锁b,试图获取锁a");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a) {
                    System.out.println(Thread.currentThread().getName()+" 获取锁a");
                }
            }
        },"B").start();
    }
}

3.Callable接口

创建线程的四种方式

  • 继承Thread类
  • 实现Runnable接口
  • Callable接口
  • 线程池

3.1 比较Runnable接口和Callable接口

public interface Runnable {
    public abstract void run();
}

public interface Callable<V> {
    V call() throws Exception;
}
  • 为了实现 Runnable,需要实现不返回任何内容的 run()方法,而对于 Callable,需要实现在完成时返回结果的 call()方法。
  • call()方法可以引发异常,而 run()则不能。
  • 不能直接替换 runnable,因为 Thread 类的构造方法根本没有 Callable

3.2 FutureTask

3.2.1 构造方法

  • public FutureTask(Callable callable) 创建一个FutureTask,一旦运行就执行给定的Callable
  • public FutureTask(Runnable runnable, V result) 创建一个FutureTask,一旦运行就执行给定的Ru你那边了,并安排成功完成时get返回给定的结果

3.2.2 核心原理

核心原理:在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future 对象在后台完成,所以这个类就译为未来任务。

当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态

一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。

仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法

一旦计算完成,就不能再重新开始或取消计算

get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完 成状态,然后会返回结果或者抛出异常

get 只计算一次,因此 get 方法放到最后

3.2.3 常用方法

public boolean cancel(boolean mayInterrupt)//用于停止任务。
public Object get()//抛出 InterruptedException,ExecutionException:用于获取任务的结果。
public boolean isDone()//如果任务完成,则返回 true,否则返回 false

3.2.4 使用 Callable 和 Future

class MyThread1 implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "run");
    }
}

class MyThread2 implements Callable {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "call");
        return 200;
    }
}

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        new Thread(new MyThread1(),"AA").start();

        FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread2());

        FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
            System.out.println(Thread.currentThread().getName() + " come in callable");
            return 1024;
        });

        new Thread(futureTask1, "123").start();
        new Thread(futureTask2, "235").start();

        System.out.println(futureTask2.get());
    }
}

总结:在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future 对象在后台完成, 当主线程将来需要时,就可以通过 Future对象获得后台作业的计算结果或者执行状态

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值