java多线程系列-----1.多线程快速入门,线程安全,多线程之间通讯,并发包,并发队列,线程池原理,锁的深度化

一. 多线程快速入门

二. 多线程线程安全

三. 多线程之间通讯

四. Java并发包&并发队列

  • 四.1.并发包
    • Vector与ArrayList区别:
    • HasTable与HasMap
    • synchronizedMap
    • ConcurrentHashMap
  • 四.2.并发队列
    • ConcurrentLinkedDeque
    • BlockingQueue
      • PriorityBlockingQueue
      • LinkedBlockingQueue
      • ArrayBlockingQueue

五. 线程池原理剖析&锁的深度化

1.面试题

1.进程与线程的区别?

答:进程是所有线程的集合,每一个线程是进程中的一条执行路径,线程只是一条执行路径。

2.为什么要用多线程?

 答:提高程序效率

3.多线程创建方式?

  答:继承Thread或Runnable 接口。

4.是继承Thread类好还是实现Runnable接口好?

答:Runnable接口好,因为实现了接口还可以继续继承。继承Thread类不能再继承。

5.你在哪里用到了多线程?

答:主要能体现到多线程提高程序效率。举例:分批发送短信、迅雷多线程下载等。

2.多线程创建方式

  1. 继承Thread类 重写run方法

    class CreateThread extends Thread {
    // run方法中编写 多线程需要执行的代码
    publicvoid run() {
    	for (inti = 0; i< 10; i++) {
    		System.out.println("i:" + i);
    	}
    }
    }
    publicclass ThreadDemo {
    
    publicstaticvoid main(String[] args) {
    	System.out.println("-----多线程创建开始-----");
    	// 1.创建一个线程
    	CreateThread createThread = new CreateThread();
    	// 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
    	System.out.println("-----多线程创建启动-----");
    	createThread.start();
    	System.out.println("-----多线程创建结束-----");
    }
    }
    
  2. 实现Runnable接口,重写run方法

    class CreateRunnable implements Runnable {
    
    	@Override
    	publicvoid run() {
    		for (inti = 0; i< 10; i++) {
    			System.out.println("i:" + i);
    		}
    	}
    }
    
    publicclass ThreadDemo2 {
    	publicstaticvoid main(String[] args) {
    		System.out.println("-----多线程创建开始-----");
    		// 1.创建一个线程
    		CreateRunnable createThread = new CreateRunnable();
    		// 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
    		System.out.println("-----多线程创建启动-----");
    		Thread thread = new Thread(createThread);
    		thread.start();
    		System.out.println("-----多线程创建结束-----");
    	}
    }
    
  3. 使用匿名内部类方式

     System.out.println("-----多线程创建开始-----");
    	 Thread thread = new Thread(new Runnable() {
    		public void run() {
    			for (int i = 0; i< 10; i++) {
    				System.out.println("i:" + i);
    			}
    		}
    	});
    	 thread.start();
    	 System.out.println("-----多线程创建结束-----");
    

    注意:
    使用继承Thread类还是使用实现Runnable接口好?
    使用实现实现Runnable接口好,原因实现了接口还可以继续继承,继承了类不能再继承。
    启动线程是使用调用start方法还是run方法?
    开始执行线程 注意 开启线程不是调用run方法,而是start方法
    调用run知识使用实例调用方法。

常用线程API:

start()启动线程
getID()Thread-编号 该编号从0开始
getName()获取当前线程名称
Stop()停止线程,
sleep(long mill)休眠线程
Thread()分配一个新的 Thread 对象
Thread(String name)分配一个新的 Thread对象,具有指定的 name正如其名。
Thread(Runable r)分配一个新的 Thread对象
Thread(Runable r, String name)分配一个新的 Thread对象

守护线程:
Java中有两种线程,一种是用户线程,另一种是守护线程。
用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止
守护线程当进程不存在或主线程停止,守护线程也会被停止。
使用setDaemon(true)方法设置为守护线程

3.多线程运行状态

在这里插入图片描述
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。

  1. 新建状态
    当用new操作符创建一个线程时, 例如new Thread®,线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码

  2. 就绪状态
    一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
    处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。

  3. 运行状态
    当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.

  4. 阻塞状态
    线程运行过程中,可能由于各种原因进入阻塞状态:
    1>线程通过调用sleep方法进入睡眠状态;
    2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
    3>线程试图得到一个锁,而该锁正被其他线程持有;
    4>线程在等待某个触发条件;

  5. 死亡状态
    有两个原因会导致线程死亡:

    1. run方法正常退出而自然死亡,
    2. 一个未捕获的异常终止了run方法而使线程猝死。
      为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.

4.jion()和Yield()

  1. jion()方法
    join作用是让其他线程变为等待, t1.join();// 让其他线程变为等待,直到当前t1线程执行完毕,才释放。
    thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B
  2. Yield方法
    Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
    yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
    结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

优先级
现代操作系统基本采用时分的形式调度运行的线程,线程分配得到的时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。下面是源码(基于1.8)中关于priority的一些量和方法。

1.什么是线程安全,线程安全的解决办法,死锁

  1. 线程安全问题:
    当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。

  2. 线程安全解决办法:
    如何解决多线程之间线程安全问题?
    使用多线程之间同步synchronized或使用锁(lock)。
    为什么使用线程同步或使用锁能解决线程安全问题呢?
    将可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,让后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
    什么是多线程之间同步?
    当多个线程共享同一个资源,不会受到其他线程的干扰。

    1. 同步代码块
      什么是同步代码块?
      就是将可能会发生线程安全问题的代码,给包括起来。

         synchronized(同一个数据){
      	     可能会发生线程冲突问题
      	    }
      	    就是同步代码块 
      	    synchronized(对象)//这个对象可以为任意对象 
      	    { 
      	        需要被同步的代码 
      	    } 
      

      对象如同锁,持有锁的线程可以在同步中执行
      没持有锁的线程即使获取CPU的执行权,也进不去
      同步的前提:
      1,必须要有两个或者两个以上的线程
      2,必须是多个线程使用同一个锁
      必须保证同步中只能有一个线程在运行
      好处:解决了多线程的安全问题
      弊端:多个线程需要判断锁,较为消耗资源、抢锁的资源。

    2. 同步函数
      什么是同步函数?
      在方法上修饰synchronized 称为同步函数

      public synchronized void sale() {
      	if (trainCount > 0) { 
      try {
      					Thread.sleep(40);
      				} catch (Exception e) {
      				}
      				System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "张票.");
      				trainCount--;
      			}
      	}
      

      同步函数用的是什么锁?
      同步函数使用this锁。
      证明方式: 一个线程使用同步代码块(this明锁),另一个线程使用同步函数。如果两个线程抢票不能实现同步,那么会出现数据错误。

    3. 静态同步函数
      什么是静态同步函数?
      方法上加上static关键字,使用synchronized 关键字修饰 或者使用类.class文件。
      静态的同步函数使用的锁是 该函数所属字节码文件对象
      可以用 getClass方法获取,也可以用当前 类名.class 表示。

      synchronized (ThreadTrain.class) {
      			System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "张票.");
      			trainCount--;
      			try {
      				Thread.sleep(100);
      			} catch (Exception e) {
      			}
      }
      
      

      总结

      synchronized 修饰方法使用锁是当前this锁。
      synchronized 修饰静态方法使用锁是当前类的字节码文件

  3. 死锁

    什么是多线程死锁?
    同步中嵌套同步,导致锁无法释放

2.多线程有三大特性

  1. 原子性
    即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  2. 可见性
    当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
    若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。
  3. 有序性
    程序执行的顺序按照代码的先后顺序执行。

3.Java内存模型

共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
在这里插入图片描述
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

总结:
什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。

4.Volatile和 ThreadLocal

  1. Volatile
    Volatile 关键字的作用是变量在多个线程之间可见。

    示例代码:

    class ThreadVolatileDemo extends Thread {
    	public    boolean flag = true;
    	@Override
    	public void run() {
    		System.out.println("开始执行子线程....");
    		while (flag) {
    		}
    		System.out.println("线程停止");
    	}
    	public void setRuning(boolean flag) {
    		this.flag = flag;
    	}
    
    }
    
    public class ThreadVolatile {
    	public static void main(String[] args) throws InterruptedException {
    		ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
    		threadVolatileDemo.start();
    		Thread.sleep(3000);
    		threadVolatileDemo.setRuning(false);
    		System.out.println("flag 已经设置成false");
    		Thread.sleep(1000);
    		System.out.println(threadVolatileDemo.flag);
    
    	}
    }
    

    运行结果:
    在这里插入图片描述
    已经将结果设置为fasle为什么?还一直在运行呢。
    原因:线程之间是不可见的,读取的是副本,没有及时读取到主内存结果。
    解决办法使用Volatile关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存” 中取值

    Volatile非原子性
    Volatile不用具备原子性

    AtomicInteger原子类
    AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减。

    public class VolatileNoAtomic extends Thread {
    static int count = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    
    @Override
    public void run() {
    	for (int i = 0; i < 1000; i++) {
    		//等同于i++
    		atomicInteger.incrementAndGet();
    	}
    	System.out.println(count);
    }
    
    public static void main(String[] args) {
    	// 初始化10个线程
    	VolatileNoAtomic[] volatileNoAtomic = new VolatileNoAtomic[10];
    	for (int i = 0; i < 10; i++) {
    		// 创建
    		volatileNoAtomic[i] = new VolatileNoAtomic();
    	}
    	for (int i = 0; i < volatileNoAtomic.length; i++) {
    		volatileNoAtomic[i].start();
    	 }
     	}
    }
    

volatile与synchronized区别:
仅靠volatile不能保证线程的安全性。(原子性)
①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
线程安全性
线程安全性包括两个方面,①可见性。②原子性。
从上面自增的例子中可以看出:仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。

  1. ThreadLocal
    ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。
    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
    ThreadLocal的接口方法
    ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

    1. void set(Object value)设置当前线程的线程局部变量的值。
    2. public Object get()该方法返回当前线程所对应的线程局部变量。
    3. public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
    4. protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

    ThreadLocal实现原理
    ThreadLocal通过map集合
    Map.put(“当前线程”,值);

1.多线程通讯

多线程之间通讯,其实就是多个线程在操作同一个资源,但是操作的动作不同。
需求:第一个线程写入(input)用户,另一个线程取读取(out)用户.实现读一个,写一个操作。

2.wait()、notify、notifyAll()方法

wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。

这三个方法最终调用的都是jvm级的native方法。随着jvm运行平台的不同可能有些许差异。
如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。
如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。
如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。
注意:一定要在线程同步中使用,并且是同一个锁的资源

wait与sleep区别?
对于sleep()方法,我们首先要知道该方法是属于Thread类中的而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
获取对象锁进入运行状态。

3.Lock

在 jdk1.5 之后,并发包中新增了 Lock 接口(以及相关实现类)用来实现锁功能,Lock 接口提供了与 synchronized 关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。

  1. Lock写法
    Lock lock  = new ReentrantLock();
    lock.lock();
    try{
    //可能会出现线程安全的操作
    }finally{
    //一定在finally中释放锁
    //也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
      lock.ublock();
    }
    
    Lock 接口与 synchronized 关键字的区别
    1. Lock 接口可以尝试非阻塞地获取锁 当前线程尝试获取锁。如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。
    2. Lock 接口能被中断地获取锁 与 synchronized 不同,获取到锁的线程能够响应中断,当获取到的锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。
    3. Lock 接口在指定的截止时间之前获取锁,如果截止时间到了依旧无法获取锁,则返回。

Condition用法:
Condition的功能类似于在传统的线程技术中的,Object.wait()和Object.notify()的功能。

	Condition condition = lock.newCondition();
	res. condition.await();  类似wait
	res. Condition. Signal() 类似notify

如何停止线程?

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  2. 使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。
  3. 使用interrupt方法中断线程。
```
public class StopThreadDemo {

public static void main(String[] args) {
	StopThread stopThread1 = new StopThread();
	Thread thread1 = new Thread(stopThread1);
	Thread thread2 = new Thread(stopThread1);
	thread1.start();
	thread2.start();
	int i = 0;
	while (true) {
		System.out.println("thread main..");
		if (i == 300) {
			// stopThread1.stopThread();
			thread1.interrupt();
			thread2.interrupt();
			break;
		}
		i++;
	}
}
}
```

1.并发包

  1. 同步容器类
    1. Vector与ArrayList区别:

      1. ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

      2. Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢
        注意: Vector线程安全、ArrayList

        Vector源码类:

         public synchronized boolean add(E e) {
            modCount++;
            ensureCapacityHelper(elementCount + 1);
            elementData[elementCount++] = e;
            return true;
        }
        

        Arraylist源码:

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

      1. HashMap不是线程安全的
        HastMap是一个接口 是map接口的子接口,是将键映射到值的对象,其中键和值都是对象,并且不能包含重复键,但可以包含重复值。HashMap允许null key和null value,而hashtable不允许。
      2. HashTable是线程安全的一个Collection。
      3. HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable。
        HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
        HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。

      注意: HashTable线程安全,HashMap线程不安全。

    3. synchronizedMap
      Collections.synchronized*(m) 将线程不安全额集合变为线程安全集合

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

2. 并发队列

在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队
列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种都继承自Queue。
在这里插入图片描述

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

    ConcurrentLinkedQueue重要方法:
    add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中这俩个方法没有任何区别)
    poll() 和peek() 都是取头元素节点,区别在于前者会删除元素,后者不会。

  2. BlockingQueue
    阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:
    在队列为空时,获取元素的线程会等待队列变为非空。
    当队列满时,存储元素的线程会等待队列可用。
    阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

    BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:

     	     当队列满了的时候进行入队列操作
     	     当队列空了的时候进行出队列操作
    

    因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。
    在Java中,BlockingQueue的接口位于java.util.concurrent 包中(在Java5版本开始提供),由上面介绍的阻塞队列的特性可知,阻塞队列是线程安全的。
    在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景。
    认识BlockingQueue
    阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
    从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
    常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)
      先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
      后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。

    多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)
    下面两幅图演示了BlockingQueue的两个常见阻塞场景:

    1. ArrayBlockingQueue
      ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。
      ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。下面是一个初始化和使用ArrayBlockingQueue的例子:

       ```
       ArrayBlockingQueue<String> arrays = new ArrayBlockingQueue<String>(3);
       arrays.add("李四");
        arrays.add("张军");
       arrays.add("张军");
       // 添加阻塞队列
       arrays.offer("张三", 1, TimeUnit.SECONDS);
       ```
      
      1. LinkedBlockingQueue
        LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。
        和ArrayBlockingQueue一样,LinkedBlockingQueue 也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。下面是一个初始化和使LinkedBlockingQueue的例子:

        LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3);
        linkedBlockingQueue.add("张三");
        linkedBlockingQueue.add("李四");
        linkedBlockingQueue.add("李四");
        System.out.println(linkedBlockingQueue.size());
        
      2. PriorityBlockingQueue
        PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注
        意,PriorityBlockingQueue中允许插入null对象。
        所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就
        是按照我们对这个接口的实现来定义的。
        另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺
        序进行迭代。
        下面我们举个例子来说明一下,首先我们定义一个对象类型,这个对象需要实现Comparable接口:

2..线程池

2.1什么是线程池

什么是线程池?
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序
都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,
还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用
线程池,必须对其实现原理了如指掌。

2.2线程池的作用分类

线程池作用:
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。
如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜。),况且我们还不能控制线程池中线程的开始、挂起、和中止。

ThreadPoolExecutor:
Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newScheduledThreadPool、newFixedThreadPool、newCachedThreadPool方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池,那么它的底层原理是怎样实现的呢,这篇就来介绍下ThreadPoolExecutor线程池的运行过程。

corePoolSize: 核心池的大小。 当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到	corePoolSize后,就会把到达的任务放到缓存队列当中
maximumPoolSize: 线程池最大线程数,它表示在线程池中最多能创建多少个线程;
keepAliveTime: 表示线程没有任务执行时最多保持多久时间会终止。
unit: 参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
2.3线程池的四种创建方式

Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:

  1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    // 无限大小线程池 jvm自动回收
    	ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
    	for (int i = 0; i < 10; i++) {
    		final int temp = i;
    		newCachedThreadPool.execute(new Runnable() {
    
    			@Override
    			public void run() {
    				try {
    					Thread.sleep(100);
    				} catch (Exception e) {
    					// TODO: handle exception
    				}
    				System.out.println(Thread.currentThread().getName() + ",i:" + temp);
    			}
    		});
    	}
    总结: 线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,
    	而不用每次新建线程。
    
  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
    	for (int i = 0; i < 10; i++) {
    		final int temp = i;
    		newFixedThreadPool.execute(new Runnable() {
    
    			@Override
    			public void run() {
    				System.out.println(Thread.currentThread().getId() + ",i:" + temp);
    			}
    		});
    	}
    总结:因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。
    定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
    
    
  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
    	for (int i = 0; i < 10; i++) {
    		final int temp = i;
    		newScheduledThreadPool.schedule(new Runnable() {
    			public void run() {
    				System.out.println("i:" + temp);
    			}
    		}, 3, TimeUnit.SECONDS);	//表示延迟3秒执行。
    }
    
  4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

线程池原理剖析
提交一个任务到线程池中,线程池的处理流程如下:

  1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
  2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
  3. 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

合理配置线程池
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
任务的优先级:高,中和低。
任务的执行时间:长,中和短。
任务的依赖性:是否依赖其他系统资源,如数据库连接。
任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务
IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数
操作系统之名称解释:
某些进程花费了绝大多数时间在计算上,而其他则在等待I/O上花费了大多是时间,
前者称为计算密集型(CPU密集型)computer-bound,后者称为I/O密集型,I/O-bound。

2..Java锁的深度化

  1. 悲观锁与乐观锁

    1. 悲观锁:悲观锁悲观的认为每一次操作都会造成更新丢失问题,在每次查询时加上排他锁。
      每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
      Select * from xxx for update;
    2. 乐观锁:乐观锁会乐观的认为每次查询都不会造成更新丢失,利用版本字段控制
  2. 重入锁
    锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利。
    重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
    在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

  3. 读写锁
    相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。

  4. CAS无锁机制

    1. 与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
    2. 无锁的好处:
      第一,在高并发的情况下,它比有锁的程序拥有更好的性能;
      第二,它天生就是死锁免疫的。
      就凭借这两个优势,就值得我们冒险尝试使用无锁的并发。
    3. CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
    4. CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
    5. 简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。
    6. 在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。
  5. 自旋锁
    自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值