多线程是提升效率性能的一个重要技术,企业级开发中运用的很普遍,我在学习了一段时间以后,仍是有不少地方是很懵比的,后续的会继续学习更新。html
1. 多线程简单的概述java
进程:正在进行的程序,好比任务管理器上的进程,QQ等等编程
线程:就是进程中负责执行程序的控制单元(或者执行路径),一个进程中有多个执行路径称之为多线程安全
开启多线程是为了同时运行多部分代码,每一个线程都有本身的运行内容,这个内容称之为线程的任务。多线程
多线程的好处:解决了多程序同时运行的问题。多线程的弊端:线程太多回到效率下降。并发
其实应用程序的执行是CPU在作着快速切换完成的,这个切换是随机的。dom
下面是多线程的几种状态ide
jdk 1.5以前的 多线程两种建立方式就很少说了。函数
1.extend Thread ,覆写run方法(run 方法就是描述的线程任务),将该类new一个对象就是建立了一个线程对象,用start()方法开启线程性能
2.实现Runnable接口,覆写run()方法,方法里面也是描述线程任务,new一个Thread线程对象,将任务构造的时候传递进去,用start()方法开启线程
注意:实现Runnable接口的好处
1.将线程的任务从线程的子类中分离出来,进行的单独的封装,按照面向对象的思想将任务进行了封装
2.避免了java单继承的局限性。
2.0 多线程安全问题
public class ticket implements Runnable{ private int num=500; public void sale(){ while (true){ synchronized(this) { if (num>0){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"...sale=="+num--); }else { break; } } } } @Override public void run() { sale(); } }
public static void main(String[] args){ //屡次启动一个线程是非法的。 ticket t1= new ticket(); Thread thread1= new Thread(t1); Thread thread2= new Thread(t1); Thread thread3= new Thread(t1); Thread thread4= new Thread(t1); thread1.start(); thread2.start(); thread3.start(); thread4.start(); }
这段代码很简单,4 个线程操做同一个ticket对象,num是一个全局变量,每一个线程进来休眠10ms后都减1,直到为0,能够观察到里面加了synchronized关键字(同步),若是不加synchronized,将会出现线程不安全的因素。
附:屡次启动一个线程对象是非法的!
打印的结果是
分析:出现这种缘由就是0线程进来后,此时num为1,判断(num>0)以后,线程休眠了,cpu释放执行权(由于cpu执行那个线程是随机切换的),1线程进来以后也出现这种状况num=1的时候,cpu释放执行权,同理其余的2个线程也发生这种状况,致使出现数据不一样步的状况,要解决这种就要加锁
线程安全问题产生的缘由:
1.多个线程操做共享数据
2.操做共享数据的代码有多条
用锁去解决线程安全的问题,synchronized(锁对象){须要被同步的代码},this表示当前线程任务对象
同步的好处: 解决了线程的安全问题
同步的弊端:相对下降了效率,由于同步外的线程都会判断同步锁。
同步的前提:同步必须有多个线程并使用同一个锁。
同步函数的锁对象是当前固定的this,同步代码块的锁是任意的对象。
静态同步函数锁的对象是该函数所属的字节码文件对象,能够用this.getClass()或者类名.class表示
死锁的状况:同步锁的的嵌套,死锁可能会出现和谐的状况。
2.1 单例模式下多线程的安全问题
/** * 懒汉式 * */ public static class Single{ private static Single s= null; private Single(){ } public static Single getInstance(){ /* * 加锁是为了解决线程的安全的问题,锁以前加个判断 是由于线程进来判断锁,比较消耗效率(解决多线程下单例的效率问题) * */ if (s==null){ synchronized (Single.class){ //多线程访问须要同步,例如a,b两个线程,a线程执行到s==null时失去了cpu的执行权, // 而后b线程同理执行到s==null失去cpu的执行权,这个就不是单例模式了。 //懒汉式在多线程状况下,操做s的共享数据有多条代码,所以存在线程不安全的状况,须要同步操做s共享数据的多条代码 if (s==null){ s= new Single(); } } } return s; } }
上述懒汉式在多线程状况下就不是单例模拟了。
2.3 多线程之间的通讯 wait和notify机制(生成者-消费者模式)
先看一下简单的例子,2个线程,一个输入线程负责读,一个输出线程负责写
/** * Created by ZC-16-012 on 2018/2/2. * * 定义多线程的资源共享数据对象 */ public class Resource { private String name; private String sex; private boolean flag=false; public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } //输入线程set方法 public synchronized void set(String name,String sex){ if (flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { this.name= name; this.sex= sex; flag=true; this.notify(); } } //输出线程 public synchronized void out(){ if (!flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { System.out.println(this.getName()+"===="+this.getSex()); this.setFlag(false); this.notify(); } } }
public class InputTask implements Runnable{ Resource r; public InputTask(Resource r){ this.r = r; } @Override public void run() { int x=0; while (true){ /** * 分析:因为资源Resource是共享数据,当输入线程获取的cpu执行权时,第一次赋值name是weiAnKang,sex是男, * 第二次输入线程再次赋值name 是'丽丽'时,因为线程之间是cpu执行权的切换,此时输出线程获取cpu的执行权, * 输出线程的结果就是'丽丽====男',这样致使多线程操做同一个资源时出现的安全问题 * */ //todo:此时在输入线程加上同步锁依然没有解决,Resource共享数据同步的问题,出现这种问题应该考虑多个线程之间是否是判断同一个锁 if (x==0){ r.set("weiAnKang","nan"); }else { r.set("丽丽","女女女"); } x=(x+1)%2; } } }
/** * Created by ZC-16-012 on 2018/2/2. * * 定义输出线程的任务 */ public class OutputTask implements Runnable{ Resource r; public OutputTask(Resource r){ this.r=r; } @Override public void run() { while (true){ r.out(); } } }
/** * Created by ZC-16-012 on 2018/2/2. * * 线程之间的通讯 * * 等待/唤醒机制 * wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中 * notify():唤醒线程池中的一个线程(任意的) * notifyAll():唤醒线程池中的全部线程 * 这些方法必须定义在同步中,由于这些方法是用于操做线程的状态的方法, * 必需要明确到底操做的是那个锁上的线程 * */ public class ThreadCommunicationDemo { public static void main(String[] args){ Resource r= new Resource(); InputTask input= new InputTask(r); OutputTask output= new OutputTask(r); //建立线程,并将线程的任务传递给线程对象 Thread t1= new Thread(input); Thread t2= new Thread(output); //开启线程 t1.start(); t2.start(); } }
等待/唤醒机制
* wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中
* notify():唤醒线程池中的一个线程(任意的)
* notifyAll():唤醒线程池中的全部线程
* 这些方法必须定义在同步中,由于这些方法是用于操做线程的状态的方法,
* 必需要明确到底操做的是那个锁上的线程
2.4 jdk1.5以后的java.util.concurrent.locks.Lock的应用
假若有多个线程进行读的操做,多个线程进行写的操做呢
/** * Created by ZC-16-012 on 2018/2/5. * * 生产者和消费者 * while判断标记,解决了线程获取cpu执行权后,是否要执行 * notifyAll 解决了本方线程必定会唤醒对方线程 */ public class ResourceClass { private String name; private int count=1; private boolean flag=false; private Lock lock= new ReentrantLock();//使用jdk1.5版本以后的锁对象 //经过已有的锁对象获取该锁上的监视器对象,建立两组监视器,一组监视生产者,一组监视消费者 Condition producerCon= lock.newCondition(); Condition consumerCon= lock.newCondition(); /** * 分析:0,1 是生产者线程,2 3是消费者线程,假设0先获取cpu的执行权,2线程消费,有一种状况当0,1线程 * t0(活),notify以后正好是 唤醒t1 线程,t1判断flag=true,t0,t1一直在while循环中等待,出现了死锁状况 * 出现这种状况就是生成者线程唤醒的是另外一个生成者线程,应该生成者要保证唤醒的至少有消费者线程 * */ /** * 对于synchronized 修饰的同步代码块,锁是隐式的 * 可使用interrupt()方法将线程从冻结状态强制恢复到运行状态中,让线程具备cpu的执行资格 * 该强制动做会发生 InterruptedException 异常 * * */ public void set(String name) { lock.lock(); try { while (flag) { try { // this.wait();// t0(活),notify 正好唤醒t1 //经过condition的await 让线程冻结 producerCon.await(); } catch (InterruptedException e) { e.printStackTrace(); } } this.name = name + count; count++; System.out.println(Thread.currentThread().getName() + "..生产者.." + this.name); flag = true; // notifyAll();//唤醒了全部线程池中等待的线程 //经过condition的signalAll 唤醒消费者的全部线程 consumerCon.signal(); } finally { lock.unlock();//释放锁 } } public void out (){ lock.lock(); try { if (!flag) { try { // this.wait(); consumerCon.await(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System.out.println(Thread.currentThread().getName() + ".......消费者.." + this.name); flag = false; // notifyAll(); //消费者唤醒全部生成者的线程 producerCon.signal(); } } finally { lock.unlock(); } } }
public class Producer implements Runnable{ ResourceClass r; public Producer(ResourceClass r){ this.r=r; } @Override public void run() { while (true){ r.set("馒头"); } } }
public class Consumer implements Runnable{ ResourceClass r; public Consumer(ResourceClass r){ this.r = r; } @Override public void run() { while (true){ r.out(); } } }
/** * Created by ZC-16-012 on 2018/2/5. */ public class ProducerAndConsumer { /** * Thread.currentThread().toString() 会打印线程的名称,优先级,线程组 * 线程的优先级 MAX_PRIORITY 10,MIN_PRIORITY 1, NORM_PRIORITY 5,数字越高,表示优先级越大, * cpu执行该线程的几率也就越大,例如:t0.setPriority(Thread.MAX_PRIORITY); 将t0线程的优先级设置成最大 * * */ public static void main(String[] args){ ResourceClass r= new ResourceClass(); Producer pro= new Producer(r); Consumer con= new Consumer(r); Thread t0 = new Thread(pro); Thread t1 = new Thread(pro); Thread t2 = new Thread(con); Thread t3 = new Thread(con); t0.start(); /** * try { t0.join(); //t0线程要申请加入进来,运行。让这个临时线程先执行完 } catch (InterruptedException e) { e.printStackTrace(); } * */ //t0.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); /** * t2.setDaemon(true) 设置成守护线程(后台线程),前台线程其余线程结束,这个线程也结束 * */ t3.start(); } }
以上例子即是多生产者多消费者的例子
3.0 volatile关键字
不少时候不会区分volatile和synchronized,先看一下这个例子
public class RunThread extends Thread{ private boolean flag = true; public void setFlag(boolean flag){ this.flag=flag; } @Override public void run() { while (flag){ } System.out.println(Thread.currentThread().getName() + "...线程运行结束"); } /** * 分析:main方法中建立了t0线程,休眠3s后,将t线程中的全局变量设置了false,主线程结束后,却没有打印t0线程中的 "...线程运行结束" * 说明t0线程一直while循环中运行,对于t0线程而言,flag的值并无改变 * * 总结:在java中,每个线程都有一个内存区,其中存放着全部线程共享的主内存中的变量值的拷贝,当线程执行时,他在本身的工做 * 内存区中操做这些变量,为了存取一个共享的变量,一个线程一般先获取锁定并去清楚它的内存工做区,把这些变量从全部线程的共享内存区中 * 正确的装入到他本身所在的工做内存区中,当线程解锁时保证该工做内存区中变量的值写回到共享内存中 * * volatile 关键字强制线程到主内存中(共享内存中)去读取变量,而不是去线程工做内存区中读取,从而实现了多个线程见的变量可见 * */ public static void main(String[] args) throws InterruptedException { RunThread t0= new RunThread(); t0.start(); Thread.sleep(3000); t0.setFlag(false); System.out.println("flag的值已经被设置成了false。。"); Thread.sleep(1000); System.out.println(t0.flag); } }
网上也搜了不少博客,https://www.cnblogs.com/yzwall/p/6661528.html 这遍博客说的很清楚
3.0 Atomic原子类
/** * Created by ZC-16-012 on 2018/2/8. */ public class VolatileNoAtomic extends Thread{ @Override public void run() { addCount(); } /** * volatile关键字具备可见性,并不具有原子性 * Atomic原子类 能够保证结果是原子性,并不能保证方法自己是原子性 * */ // private volatile static int count; private static AtomicInteger count =new AtomicInteger(0); private static void addCount(){ for (int i=0;i<1000;i++){ // count++; count.incrementAndGet(); } System.out.println(count); } public static void main(String[] args){ VolatileNoAtomic[] arr= new VolatileNoAtomic[10]; for (int i=0;i<10;i++){ arr[i]=new VolatileNoAtomic(); } for (int i=0;i<10;i++){ arr[i].start(); } } }
能够经过这个例子看一下Atomic对象和 volatile关键字的一些区别
3.1 网上有一个联系题 有4个线程分别获取C D E F 盘的大小,第5个线程统计总大小
public class ContactThread { public static void main(String[] args){ /** * CountDownLatch 是一个同步倒数计数器,构造时传入int参数,每调用一次countDown()方法 * 计数器减1 * */ final CountDownLatch countDownLatch= new CountDownLatch(4); ExecutorService executorService= Executors.newFixedThreadPool(6); final DiskMemory diskMemory= new DiskMemory(); for (int i=0;i<4;i++) executorService.execute(new Runnable() { @Override public void run() { int timer = new Random().nextInt(5); try { Thread.sleep(timer * 1000); } catch (InterruptedException e) { e.printStackTrace(); } int diskSize = diskMemory.getSize(); System.out.printf("完成磁盘的统计任务,耗时%d秒,磁盘大小为%d.\n", timer, diskSize); //统计每一个磁盘的大小 diskMemory.setSize(diskSize); //任务完成后,计数器减一 countDownLatch.countDown(); System.out.println("count num=" + countDownLatch.getCount()); } }); //主线程一直被阻塞,直到全部的线程统计完 try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.printf("所有线程统计完,全部磁盘总大小.\n" + ",totalSize = " + diskMemory.getTotalSize()); executorService.shutdown(); } }
/** * Created by ZC-16-012 on 2018/2/27. * * 定义磁盘对象 */ public class DiskMemory { private int totalSize; public int getSize(){ return (new Random().nextInt(3)+1)*100; } public void setSize(int size){ totalSize+=size; } public int getTotalSize(){ return totalSize; } }
这边用到了java.util.concurrent.CountDownLatch的一个同步倒数计数器。
固然java并发编程是一个很复杂的技术,远远不止于这些,后面学习到会陆续更新,好比java线程池的运用。
3.2synchronized关键字总结
上述synchronized关键字经过锁的机制,保证了多线程之间共享变量的安全。那么synchronized关键字何时释放锁?
要么synchronized里面的代码执行完,要么执行过程当中出现异常,synchronized 都将自动释放锁。synchronized 具备可重入性和不可中断性。
可重入性:指的是同一线程的外层函数获取到锁后,内层函数能够直接再次持有该锁。
不可中断性:指的是当前线程一旦持有该锁,其余线程只能等待,直到当前线程释放锁。
能够看出来,synchronized关键字 经过指令monitorenter,monitorexit等指令进行控制锁,monitorenter=0的时候说明该线程没有持有锁,一旦持有锁,就经过该计数器加1,进入内层函数也是加1,同理monitorexit 进行减1,执行完计算器等于0.
可见性:因为Java线程内存模型,每一个线程的线程内存 存储的是主内存(共享内存)变量的一个副本,可见性保证了当前线程内存存储的变量副本和主内存一致(读写)。
synchronized关键字进入锁以前,先把共享变量从主内存中读取出来,释放锁以前再把共享变量从当前线程内存中写到主内存中。
synchronized 的缺陷:
1.效率低:锁释放的状况少,试图得到锁时不能设置超时,不能中断一个正在试图得到锁的过程
2.不够灵活:加锁和释放锁的时机单一,好比读写锁,读的操做不会加锁,写的时候加锁
3.没法知道是否成功获取到锁。还应该避免死锁