JUC核心类深度解析

1.使用CopyOnwrite实现并发操作

进行并发读写

package juc;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/*
 * CopyOnWriteArrayList/CopyOnWriteArraySet : “写入并复制”
 * 注意:添加操作多时,效率低,因为每次添加时都会进行复制,开销非常的大。并发迭代操作多时可以选择。
 */
public class TestCopyOnWriteArrayList {
    public static void main(String[] args) {
        /*
            java.util.ArrayList
         */
        ArrayList<String> names =  new   ArrayList<String>();
//        ArrayList<String> names =  new ArrayList<>();
//        Vector<String> names = new Vector<>() ;//1.5
        names.add("zs") ;
        names.add("ls") ;
        names.add("ww") ;
        Iterator<String> iter = names.iterator();
        while(iter.hasNext()) {
             System.out.println(iter.next());//仅仅对集合进行 读操作,不会有异常
             names.add("x");//仅仅对集合进行 写操作:因为ArrayList会动态扩容(1.5倍),因此names会无限扩大,因此会包
        }
    }
}

程序出现异常

zs
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at juc.TestCopyOnWriteArrayList.main(TestCopyOnWriteArrayList.java:24)

ConcurrentModifcationException产生原因

  • ArrayList存在全局变量modCount(继承自AbstractList)并在ArrayList的内部类ITR中有一个exceptedModCount变量
  • 当ArrayList进行迭代是,迭代器会先确保modCount和expectedModCount的一致性,如果不一致就抛出异常(与CAS算法较为相似的确认了数据的一致性)
  • 上述例子中在迭代的同时向集合中添加元素(modCount++)导致modCount != expectedModCount,最终抛出异常。
java.util.ArrayList 907-910
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
modCount != expectedModCount这种确定代码一致性的操作被称为“fail-fast”策略,

修改为

 CopyOnWriteArrayList<String> names =  new   CopyOnWriteArrayList<String>();
zs
ls
ww

解决办法为将同步类容器修改为并发类容器

同步类容器并发类容器
HashTableConcurrentHashMap
vecttorCopyOnWriteArrayList

CopyOnWrite容器

容器包括了

  • CopyOnWriteArrayList
  • CopyOnWriteArraySet

操作原理为在遇到写操作时:

  1. 将当前容器复制一份,想新的容器中添加元素。
  2. 添加玩元素后更改原容器的引用指向新元素,原容器等待GC收集。

思考

利用冗余实现了读写分离,使得在读操作的时候依然可以进行写操作,不影响读操作时的数据。但是这种读写分离的方式需要使用容器的拷贝,写入操作过多的话就会非常影响性能。

2.使用ReadWriteLock实现读写锁

JUC提供的专门的读写锁,可以分贝用于对读操作或写操作进行加锁。

java.util.concurrent.locks.ReadWriteLock;

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

假设两线程:线程t1线程t2

  1. readLock():
    添加了读锁的资源可以被多个线程同时读
    • 如果t1已经获取了读锁,此时t2想要进行写操作
    • t2需要等待t1释放读锁
    • t1与t2可以同时对资源添加读锁,同时进行读操作
    • 如果其他线程需要进行写操作,要等待资源上说有的读锁消失
  2. writeLock():
    写锁,独占锁。添加写锁之后不能在被其他线程读或写

示例

public class TestReadWriteLock {
	// 读写锁
	private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	public static void main(String[] args) {
		TestReadWriteLock test = new TestReadWriteLock();
		//t1线程
		new Thread(() -> {
			//读操作
			test.myRead(Thread.currentThread());
			//写操作
			test.myWrite(Thread.currentThread());
		},"t1").start();
		
		//t2线程
		new Thread(() -> {
			//读操作
			test.myRead(Thread.currentThread());
			//写操作
			test.myWrite(Thread.currentThread());
		},"t2").start();

	}

	//用读锁来锁定读操作
	public void myRead(Thread thread) {
		rwl.readLock().lock();
		try {
			for (int i = 0; i < 10000; i++) {
				System.out.println(thread.getName() + "正在进行读操作");
			}
			System.out.println(thread.getName() + "===读操作完毕===");
		} finally {
			rwl.readLock().unlock();
		}
	}

	//用写锁来锁定写操作
	public void myWrite(Thread thread) {
		rwl.writeLock().lock();
		try {
			for (int i = 0; i < 10000; i++) {
				System.out.println(thread.getName() + "正在进行写操作");
			}
			System.out.println(thread.getName() + "===写操作完毕===");
		} finally {
			rwl.writeLock().unlock();
		}
	}
}

3.ConcurrentHashMap底层结果与演变过程

JDK7中的ConcurrentHashMap

采用数组+链表来实现,总体上来说是HashMap是一个数组,每一个数组元素是一张链表,向HashMap中写入数据时,会将元素的Key的hash值计算出元素在数组中的存储位置,具有相同hash值的元素被存放在相同的序列号的链表中。

JDK8之前,ConcurrentHashMap简介实现了Map<K,V>,将每一个元素成为segment,每个segment都是一个HashEntry<K,V>,然后对存入的链表数据进行重新的粒度划分,相同的在此存储为一个链表

默认情况下ConcurrentHashMap会生成16个segment.

在进行多线程访问时,对被访问的segment设置锁,其他segment可以被正常访问,减少访问锁带来的线程冲突。

JDK8中ConcurrentHashMap

JDK8中的HashMap进行了优化,当链表中的元素超过8个时链表会转换为红黑数,其余不变。只有超过了8个节点的链表会转换为红黑树
同时
ConcurrentHashMap也进行了同样的结构优化。
废弃了segment的加锁操作,对每一条数据使用volatile避免冲突,进行每一个元素的同步,更加细分了粒度。

4.BlockingQueue 实现排序和定时任务

BlockingQueue是JUC提供的一个用于控制线程同步的队列,可以给队列中的元素添加定时任务等功能。

方法简介
boolean add()向队列中添加元素,有剩余空间返回True否则抛出异常
boolean offer()向队列中添加元素,有剩余空间返回True没有返回False
void put(E e)向队列中添加元素,吐过还有声韵空间则直接入队,否则当前线程一直等待知道有空闲位置时加入
E poll()取出队列对首的元素,若队列为空泽惠等待一段时间,若在等待时间队列依然为空返回NULL
E take()取出排在队列队首的元素,若队列为空则一直等待
boolean remove(Obeject o)删除队列的o元素
int remainingCapacity()返回队列社会观念与容量,线程不安全,数据不真实

BlockingQueue的实现类分为有界队列和误解队列两种,有界队列是指队列中更多元素个数是有限的,而无界队列是指队列中元素个数可以是无穷多个的。

实现类

  1. ArrayBlockingQueue:由数组构成的有界阻塞队列,队列的大小由构造方法的参数决定。
  2. LinkedBlockingQueue:由链表结构组成的有界阻塞队列。可以通过构造方法的参数指定队列的大小,无参构造方法使用默认大小Integer.MAX_VALUE。
  3. PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列,排序规则可以通过构造方法中的Comparator对象指定
  4. DelayQueue:一个支持延迟存取的无界队列,如队列中的某个元素必须在第一时间后才能被取出

延迟阻塞队列示例 DelayQueue

假设三人进入游泳馆,各自游泳的时间为30分钟,45分钟,60分钟,时间到了自动离开
游泳者类

public class Swimmer implements Delayed {

	private String name;
	private long endTime;

	public Swimmer() {
	}

	public Swimmer(String name, long endTime) {
		this.name = name;
		this.endTime = endTime;
	}

	public String getName() {
		return this.name;
	}

	/*
	   是否还有剩余时间。
	   如果返回正数,表明还有剩余时间;
	   如果返回0或者负数,说明已超时;超时时,才会让DelayQueue的take()方法真正取出元素。
	 */
	@Override
	public long getDelay(TimeUnit unit) {
		return endTime - System.currentTimeMillis();
	}

	//线程(游泳者)之间,根据剩余时间的大小进行排序
	@Override
	public int compareTo(Delayed delayed) {
		Swimmer swimmer = (Swimmer) delayed;
		return this.getDelay(TimeUnit.SECONDS) - swimmer.getDelay(TimeUnit.SECONDS) > 0 ? 1 : 0;
	}
}

游泳馆类

public class Natatorium implements Runnable {
	// 用延迟队列模拟多个Swimmer,每个Swimmer的getDelay()方法就表示自己剩余的游泳时间
	private DelayQueue<Swimmer> queue = new DelayQueue<Swimmer>();

	// 标识游泳馆是否开业
	private volatile boolean isOpen = true;

	// 向DelayQueue中增加游泳者
	public void addSwimmer(String name, int playTime) {
		// 规定游泳的结束时间
		long endTime = System.currentTimeMillis() + playTime * 1000 * 60;
		Swimmer swimmer = new Swimmer(name, endTime);
		System.out.println(swimmer.getName() + "进入游泳馆,可供游泳时间:" + playTime + "分");
		this.queue.add(swimmer);
	}
	@Override
	public void run() {
		while (isOpen) {
			try {
				/*
				 * 注意:在DelayQueue中,take()并不会立刻取出元素。
				 * 只有当元素(Swimmer)所重写的getDelay()返回0或者负数时,才会真正取出该元素。
				 */
				Swimmer swimmer = queue.take();
				System.out.println(swimmer.getName() + "游泳时间结束");
				// 如果DelayQueue中的元素已被取完,则停止线程
				if (queue.size() == 0) {
					isOpen = false;
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

测试类

public class TestNatatorium {
	public static void main(String args[]) {
		try {
			Natatorium natatorium = new Natatorium();
			Thread nataThread = new Thread(natatorium);
			nataThread.start();
			natatorium.addSwimmer("zs", 30);
			natatorium.addSwimmer("ls", 45);
			natatorium.addSwimmer("ww", 60);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

结果

zs进入游泳馆,可供游泳时间:30分
ls进入游泳馆,可供游泳时间:45分
ww进入游泳馆,可供游泳时间:60分
zs游泳时间结束
ls游泳时间结束
ww游泳时间结束

5.通过CountDownLatch 实现多线程闭锁

CountDownLatch是个线程同步工具,可以用来协调多个线程的执行时间。可以让A线程在其他线程执行结束后再执行。
CountDownLatch可以指定等待的线程数量,在其他线程执行结束后调用,在每一个线程结束后调用countDow()方法来使计数器递减。同时A线程调用await()方法用来等待计数器为0。当计数器为0时唤醒线程执行内容。

示例

public class TestCountDownLatch {
	public static void main(String[] args) {
	    //计数器为10
		CountDownLatch countDownLatch = new CountDownLatch(10);

        //将CountDownLatch对象传递到线程的run()方法中,当每个线程执行完毕run()后就将计数器减1
        MyThread myThread = new MyThread(countDownLatch);
        long start = System.currentTimeMillis();
        //创建10个线程,并执行
		for (int i = 0; i < 10; i++) {
			new Thread(myThread).start();
		}
		try {
			//主线程(main)等待:等待的计数器为0;即当CountDownLatch中的计数器为0时,Main线程才会继续执行。
			countDownLatch.await();
		} catch (InterruptedException e) {
		    e.printStackTrace();
		}
		long end = System.currentTimeMillis();
		System.out.println("耗时:" + (end - start));
	}
}
class MyThread implements Runnable {
	private CountDownLatch latch;
	public MyThread(CountDownLatch latch) {
		this.latch = latch;
	}
	@Override
	public void run() {
		try {
			Thread.sleep(3000);
		}catch (InterruptedException e){
		    e.printStackTrace();
        }
            finally {
			latch.countDown();//每个子线程执行完毕后,触发一次countDown(),即计数器减1
		}
	}
}

结果(根据不同的计算机效果不同)

耗时:3009

思考

  1. 如果注释掉countDownLatch.await()方法:
    结果:线程取消等待,直接执行完成
  2. 如果注释掉countDown()方法:
    线程一直等待,无法被唤醒继续执行。

6.CyclicBarrier在多线程中设置屏障

用于解决多个线程之间相互等待的问题。
线程执行时会使先到达的线程执行await()方法进行等待,之后当所有需要的线程到位后一起跨越屏障(执行)

示例

public class TestCyclicBarrier {

    static class MyThread implements Runnable {
        //用于控制会议开始的屏障
        private CyclicBarrier barrier;
        //参会人员
        private String person;

        public MyThread(CyclicBarrier barrier, String name) {
            this.barrier = barrier;
            this.person = name;
        }

        @Override
        public void run() {
            try {
                Thread.sleep((int) (10000 * Math.random()));
                System.out.println(person + " 已经到会...");
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(person + " 开始会议...");
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        //将屏障设置为3,即当有3个线程执行到await()时,再同时释放
        CyclicBarrier barrier = new CyclicBarrier(3);
        ExecutorService executor = Executors.newFixedThreadPool(3);
        //三个人去开会
        executor.submit(new MyThread(barrier, "zs"));
        executor.submit(new MyThread(barrier, "ls"));
        executor.submit(new MyThread(barrier, "ww"));
        executor.shutdown();
    }
}

结果
注:有时间滞后性,运行后体会。

ww 已经到会...
zs 已经到会...
ls 已经到会...
ls 开始会议...
ww 开始会议...
zs 开始会议...

7.使用FutureTask 和Callable 实现多线程

FutureTask

Future是JDK提供的一种用于多线程环境下异步问题的一种模式。
对话:
老板:“小王,吧会议记录整理好给我”
小王:“好的”
随后,小王理科开始整理,几分钟,将文件送给老板。
Future模式 就是以上情景的体现。客户端向服务端发起请求,服务端会立即向客户端发出回复,客户端拿到假的返回结果,当服务端处理完成后将结果返回给客户端。
优点:客户端在发出请求后可以马上拿到结果,不需要等待。
使用FutureTask时,使用get()获取最终的返回结果。get()是一个阻塞方法,会一直等待返回值出现。

Callable

类似于Runnalbe方法,需要重写一个 Call() 方法。拥有一个泛型返回值。通过start(()调用。

组合使用

public class TestCallable {

    public static void main(String[] args) {
        //创建一个Callable类型的线程对象
        MyCallableThread myThread = new MyCallableThread();
        //将线程对象包装成FutureTask对象,用于接收线程的返回值
        FutureTask<Integer> result = new FutureTask<>(myThread);
        //运行线程
        new Thread(result).start();
        //通过FutureTask的get()接收myThread的返回值
        try {
            Integer sum = result.get();//以闭锁的方式,获取线程的返回值
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class MyCallableThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("线程运行中...计算1-100之和");
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        Thread.sleep(3000);
        return sum;
    }
}

结果

线程运行中...计算1-100之和
5050
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值