多线程笔记(二)

线程之间通信

线程通信概念:线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程间的通信就是成为整体的比用方式之一。当线程存在通信指挥,系统间的交互性会更强大,在提高CPU利用率的同时还会使开发人员对线程认为在处理的过程中进行有效的把控与监督。
使用wait、notify方法实现线程间的通信。这两个方法都是object的类方法,既所有对象都提供了这两个方法。
1.wait和notify必须配合synchronized关键字使用
3.wait方法释放锁,notify不释放锁。 实例:

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

public class ListAdd1 {

private volatile static List list = new ArrayList();    

public void add(){
    list.add("bjsxt");
}
public int size(){
    return list.size();
}

public static void main(String[] args) {

    final ListAdd1 list1 = new ListAdd1();

    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                for(int i = 0; i <10; i++){
                    list1.add();
                    System.out.println("当前线程:" + Thread.currentThread().getName() + "添加了一个元素..");
                    Thread.sleep(500);
                }   
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "t1");

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            while(true){
                if(list1.size() == 5){
                    System.out.println("当前线程收到通知:" + Thread.currentThread().getName() + " list size = 5 线程停止..");
                    throw new RuntimeException();
                }
            }
        }
    }, "t2");       

    t1.start();
    t2.start();
}

}
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* @author alienware
*
*/
public class ListAdd2 {
private volatile static List list = new ArrayList();

public void add(){
    list.add("bjsxt");
}
public int size(){
    return list.size();
}

public static void main(String[] args) {

    final ListAdd2 list2 = new ListAdd2();
    final Object lock = new Object();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                synchronized (lock) {
                    System.out.println("t1启动..");
                    for(int i = 0; i <10; i++){
                        list2.add();
                        System.out.println("当前线程:" + Thread.currentThread().getName() + "添加了一个元素..");
                        Thread.sleep(500);
                        if(list2.size() == 5){
                            System.out.println("已经发出通知..");
                            lock.notify();
                        }
                    }                       
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }, "t1");

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("t2启动..");
                if(list2.size() != 5){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("当前线程:" + Thread.currentThread().getName() + "收到通知线程停止..");
                throw new RuntimeException();
            }
        }
    }, "t2");   
    t2.start();
    t1.start();

}

}
2.2使用wait、notify模拟Queue BlockingQueue:是一个阻塞队列,阻塞的放入和得到数据。我们要是实现LinkedBlockingQueue下面连个简单的方法put和take。

import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 模拟Queue
* @author alienware
*
*/
public class MyQueue {

private final LinkedList<Object> list = new LinkedList<Object>();

private final AtomicInteger count = new AtomicInteger(0);

private final int maxSize;
private final int minSize = 0;

private final Object lock = new Object();

public MyQueue (int maxSize){
    this.maxSize = maxSize;
}

public void put (Object obj) {
    synchronized(lock){
        while(count.get() == maxSize){
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        list.add(obj);
        count.getAndIncrement();
        System.out.println(" 元素 " + obj + " 被添加 ");
        lock.notify();

    }
}

public Object take(){
    Object temp = null;
    synchronized (lock) {
        while(count.get() == minSize){
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count.getAndDecrement();
        temp = list.removeFirst();
        System.out.println(" 元素 " + temp + " 被消费 ");
        lock.notify();
    }
    return temp;
}

public int size(){
    return count.get();
}


public static void main(String[] args) throws Exception {

    final MyQueue m = new MyQueue(5);
    m.put("a");
    m.put("b");
    m.put("c");
    m.put("d");
    m.put("e");
    System.out.println("当前元素个数:" + m.size());
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            m.put("h");
            m.put("i");
        }
    }, "t1");

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                Object t1 = m.take();
                //System.out.println("被取走的元素为:" + t1);
                Thread.sleep(1000);
                Object t2 = m.take();
                //System.out.println("被取走的元素为:" + t2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "t2");

    t1.start();
    Thread.sleep(1000);
    t2.start();

}

}
2.3 ThreadLocal

早在JDK 1.2的版本中就提供Java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。   当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。   从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。   所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。
  从性能上说,ThreadLocal不具有绝对的优势,在并发不是很高的时候,加锁的性能会更好,但作为一套与锁完全无关的线程安全解决方法,在高并发量或者竞争激烈的场景,使用threadLocal可以在一定程度上减少锁竞。

3.1同步类容器

同步类容器都是线程安全的,但在某些场景下可能需要加锁来保护复合操作。复合类操作如:迭代(反复访问元素,遍历完容器中的所有元素)、跳转(根据指定的顺序找到当前元素的下一个元素)、以及条件运算。这些复合操作在多线程并发的修改容器时,可能会表现出意外的行为,最经典的便是ConcurrentModificationException,原因是当容器迭代的过程中,被并发的修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改的问题。

同步类容器:如古老的Vector/HashTable。这些容器的同步功能其实都是有JDK的Collections.syncronized等工厂方法区创建实现的。其底层的机制无非就是用传统的synchronized关键字对每个公用的方法都进行同步,使得每次只能有一个线程访问容器的状态。这很明显不满足我们今天互联网时代高并发的需求,在保证线程安全的同时,也必须有足够好的性能。

3.2并发类容器

jdk5.0以后提供了多种并发类容器来替代同步类从而改善性能。同步类容器的状态都是串行化的。他们虽然实现了线程安全,但是严重降低了并发性,在多线程环境时,严重降低了应用的吞吐量。

并发类容器时专门针对并发设计的,使用ConcurrentHashMap来代替给予散列的传统的HashTable,而且在ConcurrentHashMap中,添加了一些常见复合操作的支持。以及使用了CopyOnWriteArryList代替vector,并发的CopyonWriteArraySet以及并发的queue,ConcurrentLinkedQueue和LinkedBlockingQueue,前者是高性能的队列,后者是以阻塞形式的队列,具体实现Queue还有很多,例如ArrayBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。

4.1 ConcurrentMap

ConcurrentMap接口下有两个重要的实现:
ConcurrentHashMap
ConcurrentSkipListMap(支持并发排序功能)
ConcurrentHashMap内部使用段(segment)来表示不同的部分,每个段其实就是一个小的hashTable,他们有自己的锁,只要多个修改操作发生在不同的段上,他们就可以并发进行,把一个整体分成了16个段。也就是最高支持16个线程的并发修改操作。这也是在多线程场景时减小锁的粒度从而降低锁竞争的一种方案。并且代码中大多共享变量使用volatile关键词声明,目的就是第一时间获得修改的内容,性能非常好。

4.2 Copy-On-Write容器

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

什么是CopyOnWrite容器

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

5.1 并发Queue

在并发队列上JDK提供了两套实现,一个是以2ConcurrentLinkedQueue为代表的该性能队列,一个是BlockingQueue接口2为代表的阻塞队列,无论那种都继承自Queue。

5.2ConcurrentLinkedQueue

ConcurrentLinkedQueue:是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。他是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素。

BlockingQueue接口

阻塞队列由BlockingQueue进行定义。在jdk 1.8中实现了该接口的主要有以下几个:

ArrayBlockingQueue –基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的线程最优先能够访问队列。
LinkedBlockingQueue–基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。
PriorityBlockingQueue–无界阻塞队列,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。
DelayQueue –基于PriorityQueue实现的延迟队列,是一个无界的阻塞队列,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。因此向队列中插入时永远不会阻塞,获取时才有可能被阻塞。
SynchronousQueue –同步阻塞队列,队列大小为1,一个元素要放到该队列中必须有一个线程在等待获取元素。既生产者生产的数据直接会被消费者获得并消费。
DelayedWorkQueue –该队列为ScheduledThreadPoolExecutor中的静态内部类,ScheduledThreadPoolExecutor便是通过该队列使得队列中的元素按一定顺序排列从而时延迟任务和周期性任务得以顺利执行。
BlockingDeque–双向阻塞队列的接口。 TransferQueue–接口,定义了另一种阻塞情况:生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费,而BlockingQueue只需将元素添加到队列中后生产者便会停止被阻塞。
5.4阻塞队列常用方法

阻塞队列也实现了Queue,因此也具有上述方法并且都进行了同步处理。除此之外还有4个很有用的方法: put(E e):向队尾存入元素,如果队列满,则等待;

take():从队首取元素,如果队列为空,则等待;

offer(E e,long timeout, TimeUnit unit):向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true;

poll(long timeout, TimeUnit unit):从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取不到,则返回null;否则返回取得的元素;

5.5 deque双端队列

deque允许在队列的头部或尾部进行出队和入队操作。
LinkedBlockingDeque是一个线程安全的双端队列实现,可以说他是最为复杂的一种队列,在内部实现维护了前端和后端节点,但是没有实现读写分离,因此同一时间只能有一个线程对器进行操作。在高并发的性能要远低于其他BlockingQueue。更要低于ConcurrentLinkedQueue,在JDK早期有一个非线程安全的Deque就是ArrayDeque了,java6里添加了LinkedBlockingDeque来弥补多线程场景下线程安全的问题。

6.1 多线程的设计模式

并行设计模式属于设计优化的一部分,他是对一些常用的多线程结构的总结和抽象。与串行程序相比,并行程序的结构通常更为复杂。因此合理的使用并行模式在多线程开发中更具有意义。在这里主要介绍Future。Master-worker和生产者消费者模型。

Future模式

Futrue模式:对于多线程,如果线程A要等待线程B的结果,那么线程A没必要等待B,直到B有结果,可以先拿到一个未来的Future,等B有结果是再取真实的结果。

在多线程中经常举的一个例子就是:网络图片的下载,刚开始是通过模糊的图片来代替最后的图片,等下载图片的线程下载完图片后在替换。而在这个过程中可以做一些其他的事情。

首先客户端向服务器请求RealSubject,但是这个资源的创建是非常耗时的,怎么办呢?这种情况下,首先返回Client一个FutureSubject,以满足客户端的需求,于此同时呢,Future会通过另外一个Thread 去构造一个真正的资源,资源准备完毕之后,在给future一个通知。如果客户端急于获取这个真正的资源,那么就会阻塞客户端的其他所有线程,等待资源准备完毕。

6.3 Master-Worker模式

Master-Worker模式是常用的并行模式之一,它的核心思想是,系统有两个进程协作工作:Master进程,负责接收和分配任务;Worker进程,负责处理子任务。当Worker进程将子任务处理完成后,结果返回给Master进程,由Master进程做归纳汇总,最后得到最终的结果。 其好处是能将一个大任务分解成若干个小任务,并行执行,从而提高系统的吞吐量。
image Master-Worker模式是一种将串行任务并行化的方案,被分解的子任务在系统中可以被并行处理,同时,如果有需要,Master进程不需要等待所有子任务都完成计算,就可以根据已有的部分结果集计算最终结果集。

生产者-消费者

生产者-消费者也是一个非常经典的多线程模式。我们在实际开发中应用非常广泛的思想理念。在生产-消费模式中:通常有两类线程,既若干个生产者的线程和若干个消费者的线程。生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务,在生产者和消费者之间通过共享内存缓存区进行通信。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江北望江南

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值