任务调度
大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这就是我们所说的并发。
进程:
进程是一个具有独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序、数据集合和进程控制块三部分组成,程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需的数据和工作区;程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息是进程存在的唯一标识。
进程具有的的特征:
(1)动态性:进程是程序的一次执行过程,是临时的,有生命周期的,是动态产生、动态消亡的;
(2)并发性:任何进程都可以同其他进程一起并发执行;
(3)独立性:进程是系统进行资源分配和调度的一个独立单位;
(4)结构性:进程由程序、数据和进程控制块三部分组成;
线程:
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分配的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在的进程内存空间)。一个标准的线程由线程ID、当前指令指针(pc)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
进程与线程的区别
(1)线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
(2)一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
(3)进程之间相互独立,但同一个进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
(4)调度和切换:线程上下文切换比进程上下文切换要快的多。
多线程与多核
“同一时间点只有一个任务在执行”这句话是不准确的,至少它是不全面的。多核处理器是指在一个处理器上集成多个运算核心从而提高技术能力也就是有多个真正并行计算的处理核心,每一个处理核心对应一个内核线程。内核线程(Kernel Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进程调度,并负责将线程的任务映射到各个处理器上。一般一个处理核心对应一个内核线程,比如单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程。
超线程技术就是利用特殊的硬件指令,把一个物理芯片模拟成两个逻辑处理核心,让单个处理器都能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了CPU的闲置时间,提高了CPU的运行效率。这种超线程技术有处理器硬件决定,同时也需要操作系统的支持才能在计算机中表现出来。
进程的五种状态
(1)创建:进程正在创建,还不能运行,操作系统在创建进程时要进行的工作包括分配和建立进程控制块表项、建立资源表格并分配资源、加载程序并建立地址空间;
(2)就绪:时间片已用完,此线程被强制暂停,等待下一个属于他的时间片到来;
(3)运行:此线程正在执行,正在占用时间片;
(4)阻塞:也叫等待状态,等待某一事件(如IO或另一个线程)执行完;
(5)退出:进程已结束,所以也称结束状态,释放操作系统分配的资源。
线程总结:
1.synchronized 锁定的是对象,如果方法上有static 锁定的是class
2.同步的方法和非同步的方法是否可以同时调用,可以。非同步方法在执行的过程中不读取锁
3.对业务写方法加锁,对业务读方法不加锁容易产生脏读问题。dirtyRead 解决方案:copyOnWrite
4.一个同步方法可以调用另一个同步方法;一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。也就是说synchronized的锁是可重入的
5.synchronized出现异常会自动释放锁
6.volatile 线程间可见 synchronized 既解决了可见性也解决了原子性
7.线程中用到的自增自减,可以用到AtomicXXX类
8.syncronnized代码优化:同步代码块中的语句越少越好
9.synchronzied锁定的对象属性发生改变时不影响锁定,但如果重新new这个对象的话会影响锁--锁在堆内
10.wait进入等待状态并释放锁 notify会唤醒处于等待状态的线程,直到方法完成后才会释放锁:
当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态。当执行notify/notifyAll方法时,会唤醒处于等待该对象锁的线程,然后继续往下执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁
代码实例
1 /** 2 * 曾经的面试题:(淘宝?) 3 * 实现一个容器,提供两个方法,add,size 4 * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束 5 * 6 * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做呢? 7 * 8 * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁 9 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以 10 * 11 * 阅读下面的程序,并分析输出结果 12 * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出 13 * 想想这是为什么? 14 * 15 * notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行 16 * 整个通信过程比较繁琐 17 * @author ypy 18 */ 19 package yxxy.c_019; 20 21 import java.util.ArrayList; 22 import java.util.List; 23 import java.util.concurrent.TimeUnit; 24 25 26 public class MyContainer4 { 27 28 //添加volatile,使t2能够得到通知 29 volatile List lists = new ArrayList(); 30 31 public void add(Object o) { 32 lists.add(o); 33 } 34 35 public int size() { 36 return lists.size(); 37 } 38 39 public static void main(String[] args) { 40 MyContainer4 c = new MyContainer4(); 41 42 final Object lock = new Object(); 43 44 new Thread(() -> { 45 synchronized(lock) { 46 System.out.println("t2启动"); 47 if(c.size() != 5) { 48 try { 49 lock.wait(); 50 } catch (InterruptedException e) { 51 e.printStackTrace(); 52 } 53 } 54 System.out.println("t2 结束"); 55 //通知t1继续执行 56 lock.notify(); 57 } 58 59 }, "t2").start(); 60 61 try { 62 TimeUnit.SECONDS.sleep(1); 63 } catch (InterruptedException e1) { 64 e1.printStackTrace(); 65 } 66 67 new Thread(() -> { 68 System.out.println("t1启动"); 69 synchronized(lock) { 70 for(int i=0; i<10; i++) { 71 c.add(new Object()); 72 System.out.println("add " + i); 73 74 if(c.size() == 5) { 75 lock.notify(); 76 //释放锁,让t2得以执行 77 try { 78 lock.wait(); 79 } catch (InterruptedException e) { 80 e.printStackTrace(); 81 } 82 } 83 84 try { 85 TimeUnit.SECONDS.sleep(1); 86 } catch (InterruptedException e) { 87 e.printStackTrace(); 88 } 89 } 90 } 91 }, "t1").start(); 92 93 94 } 95 }
门闩实现方式:
门栓实现了阻塞,常用的应用场景为监控某个程序执行完成后再启动该程序,定义为:。CountDownLatch latch= new CountDownLatch(N);, 在需要等待的程序中加入latch.await();方法监控,当满足一个条件时调用latch.countDown();方法,使得N-1。当N=0是唤醒等待的程序。
1 /** 2 * 曾经的面试题:(淘宝?) 3 * 实现一个容器,提供两个方法,add,size 4 * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束 5 * 6 * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做呢? 7 * 8 * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁 9 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以 10 * 11 * 阅读下面的程序,并分析输出结果 12 * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出 13 * 想想这是为什么? 14 * 15 * notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行 16 * 整个通信过程比较繁琐 17 * 18 * 使用Latch(门闩)替代wait notify来进行通知 19 * 好处是通信方式简单,同时也可以指定等待时间 20 * 使用await和countdown方法替代wait和notify 21 * CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行 22 * 当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了 23 * 这时应该考虑countdownlatch/cyclicbarrier/semaphore 24 * @author ypy 25 */ 26 package yxxy.c_019; 27 28 import java.util.ArrayList; 29 import java.util.List; 30 import java.util.concurrent.CountDownLatch; 31 import java.util.concurrent.TimeUnit; 32 33 public class MyContainer5 { 34 35 // 添加volatile,使t2能够得到通知 36 volatile List lists = new ArrayList(); 37 38 public void add(Object o) { 39 lists.add(o); 40 } 41 42 public int size() { 43 return lists.size(); 44 } 45 46 public static void main(String[] args) { 47 MyContainer5 c = new MyContainer5(); 48 49 CountDownLatch latch = new CountDownLatch(1); 50 51 new Thread(() -> { 52 System.out.println("t2start"); 53 if (c.size() != 5) { 54 try { 55 latch.await(); 56 57 //也可以指定等待时间 58 //latch.await(5000, TimeUnit.MILLISECONDS); 59 } catch (InterruptedException e) { 60 e.printStackTrace(); 61 } 62 } 63 System.out.println("t2 end"); 64 65 }, "t2").start(); 66 67 try { 68 TimeUnit.SECONDS.sleep(1); 69 } catch (InterruptedException e1) { 70 e1.printStackTrace(); 71 } 72 73 new Thread(() -> { 74 System.out.println("t1start"); 75 for (int i = 0; i < 10; i++) { 76 c.add(new Object()); 77 System.out.println("add " + i); 78 79 if (c.size() == 5) { 80 // 打开门闩,让t2得以执行 81 latch.countDown(); 82 } 83 84 try { 85 TimeUnit.SECONDS.sleep(1); 86 } catch (InterruptedException e) { 87 e.printStackTrace(); 88 } 89 } 90 91 }, "t1").start(); 92 93 } 94 }
11.reentrantlock用于替代synchronized,比synchronized更灵活,reentrantlock需要手动释放锁;reentrantlock需要手动释放锁;reentrantlock需要手动释放锁;
使用syn锁定遇到异常的话,jvm会自动释放锁,但使用lock必须手动释放锁,因此常常在finally中进行锁的释放
Lock lock = new ReentrantLock();
lock.lock(); lock.unlock();
12.使用reentrantlock可以进行“尝试锁定”tryLock,这样无法锁定,或者在指定时间内无法锁定,线程可以决定是否继续等待
1 try { 2 boolean locked = lock.tryLock(5, TimeUnit.SECONDS); 3 if(locked){ 4 System.out.println("m2 ..." + locked); 5 } 6 } catch (InterruptedException e) { 7 e.printStackTrace(); 8 } finally { 9 if(locked) lock.unlock(); 10 }
13.使用ReentrantLock还可以调用lockInterruptibly方法,可以对线程interrupt方法做出响应,在一个线程等待锁的过程中,可以被打断
1 try { 2 lock.lockInterruptibly(); //可以对interrupt()方法做出响应 3 System.out.println("t2 start"); 4 TimeUnit.SECONDS.sleep(5); 5 System.out.println("t2 end"); 6 } catch (InterruptedException e) { 7 System.out.println("interrupted!"); 8 } finally { 9 if(lock.tryLock()){ 10 lock.unlock(); 11 } 12 }
--调用时打断当前线程 13 t2.interrupt();
14.reentranlock可以是公平锁(等待时间长的线程优先获得锁)
1 private static ReentrantLock lock=new ReentrantLock(true); //参数为true表示为公平锁,请对比输出结果 2 public void run() { 3 for(int i=0; i<10; i++) { 4 lock.lock(); 5 try{ 6 System.out.println(Thread.currentThread().getName()+"get lock"); 7 }finally{ 8 lock.unlock(); 9 } 10 } 11 } 12 public static void main(String[] args) { 13 ReentrantLock5 rl=new ReentrantLock5(); 14 Thread th1=new Thread(rl); 15 Thread th2=new Thread(rl); 16 th1.start(); 17 th2.start(); 18 }
输出结果:
1 Thread-1get lock 2 Thread-2get lock 3 Thread-1get lock 4 Thread-2get lock 5 Thread-1get lock 6 Thread-2get lock 7 Thread-1get lock 8 Thread-2get lock 9 Thread-1get lock 10 Thread-2get lock
15.面试题
15.1.wait/notifyAll方法实现
1 /** 2 * 面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法, 3 * 能够支持2个生产者线程以及10个消费者线程的阻塞调用 4 * 5 * 使用wait和notify/notifyAll来实现 6 * 7 * 使用Lock和Condition来实现 8 * 对比两种方式,Condition的方式可以更加精确的指定哪些线程被唤醒 9 * 10 */ 11 12 public class MyContainer2<T> { 13 final private LinkedList<T> lists = new LinkedList<>(); 14 final private int MAX = 10; //最多10个元素 15 private int count = 0; 16 17 private Lock lock = new ReentrantLock(); 18 private Condition producer = lock.newCondition(); 19 private Condition consumer = lock.newCondition(); 20 21 public void put(T t) { 22 try { 23 lock.lock(); 24 while(lists.size() == MAX) { //想想为什么用while而不是用if? 25 producer.await(); 26 } 27 28 lists.add(t); 29 ++count; 30 consumer.signalAll(); //通知消费者线程进行消费 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 } finally { 34 lock.unlock(); 35 } 36 } 37 38 public T get() { 39 T t = null; 40 try { 41 lock.lock(); 42 while(lists.size() == 0) { 43 consumer.await(); 44 } 45 t = lists.removeFirst(); 46 count --; 47 producer.signalAll(); //通知生产者进行生产 48 } catch (InterruptedException e) { 49 e.printStackTrace(); 50 } finally { 51 lock.unlock(); 52 } 53 return t; 54 } 55 56 public static void main(String[] args) { 57 MyContainer2<String> c = new MyContainer2<>(); 58 //启动消费者线程 59 for(int i=0; i<10; i++) { 60 new Thread(()->{ 61 for(int j=0; j<5; j++) System.out.println(c.get()); 62 }, "c" + i).start(); 63 } 64 65 try { 66 TimeUnit.SECONDS.sleep(2); 67 } catch (InterruptedException e) { 68 e.printStackTrace(); 69 } 70 71 //启动生产者线程 72 for(int i=0; i<2; i++) { 73 new Thread(()->{ 74 for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j); 75 }, "p" + i).start(); 76 } 77 } 78 }
15.2.使用Lock和Condition来实现
1 /** 2 * 面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法, 3 * 能够支持2个生产者线程以及10个消费者线程的阻塞调用 4 * 5 * 使用wait和notify/notifyAll来实现 6 * 7 * 使用Lock和Condition来实现 8 * 对比两种方式,Condition的方式可以更加精确的指定哪些线程被唤醒 9 * 10 */ 11 12 public class MyContainer2<T> { 13 final private LinkedList<T> lists = new LinkedList<>(); 14 final private int MAX = 10; //最多10个元素 15 private int count = 0; 16 17 private Lock lock = new ReentrantLock(); 18 private Condition producer = lock.newCondition(); 19 private Condition consumer = lock.newCondition(); 20 21 public void put(T t) { 22 try { 23 lock.lock(); 24 while(lists.size() == MAX) { //想想为什么用while而不是用if? 25 producer.await(); 26 } 27 28 lists.add(t); 29 ++count; 30 consumer.signalAll(); //通知消费者线程进行消费 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 } finally { 34 lock.unlock(); 35 } 36 } 37 38 public T get() { 39 T t = null; 40 try { 41 lock.lock(); 42 while(lists.size() == 0) { 43 consumer.await(); 44 } 45 t = lists.removeFirst(); 46 count --; 47 producer.signalAll(); //通知生产者进行生产 48 } catch (InterruptedException e) { 49 e.printStackTrace(); 50 } finally { 51 lock.unlock(); 52 } 53 return t; 54 } 55 56 public static void main(String[] args) { 57 MyContainer2<String> c = new MyContainer2<>(); 58 //启动消费者线程 59 for(int i=0; i<10; i++) { 60 new Thread(()->{ 61 for(int j=0; j<5; j++) System.out.println(c.get()); 62 }, "c" + i).start(); 63 } 64 65 try { 66 TimeUnit.SECONDS.sleep(2); 67 } catch (InterruptedException e) { 68 e.printStackTrace(); 69 } 70 71 //启动生产者线程 72 for(int i=0; i<2; i++) { 73 new Thread(()->{ 74 for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j); 75 }, "p" + i).start(); 76 } 77 } 78 }
16.不加锁实现懒加载
1 public class Singleton { 2 3 private Singleton() { 4 System.out.println("single"); 5 } 6 7 // 调用静态方法拿实例 8 public static Singleton getSingle() { 9 return Inner.s; 10 } 11 12 public static void main(String[] args) { 13 Thread[] ths = new Thread[200]; 14 for (int i = 0; i < ths.length; i++) { 15 ths[i] = new Thread(() -> { 16 Singleton.getSingle(); 17 }); 18 } 19 20 Arrays.asList(ths).forEach(o -> o.start()); 21 } 22 23 // 静态方法 24 private static class Inner { 25 private static Singleton s = new Singleton(); 26 } 27 28 }
17.多线程火车票面试题
17.1.使用synchronized配合wait和notifyall
1 /** 2 * 有N张火车票,每张票都有一个编号 3 * 同时有10个窗口对外售票 4 * 请写一个模拟程序 5 * 就算操作A和B都是同步的,但A和B组成的复合操作也未必是同步的,仍然需要自己进行同步 6 * 就像这个程序,判断size和进行remove必须是一整个的原子操作 7 * 8 */ 9 public class TicketSeller3 { 10 static List<String> tickets = new LinkedList<>(); 11 12 static { 13 for (int i = 0; i < 1000; i++) tickets.add("票 编号:" + i); 14 } 15 16 public static void main(String[] args) { 17 18 for (int i = 0; i < 10; i++) { 19 new Thread(() -> { 20 while (true) { 21 synchronized (tickets) { 22 if (tickets.size() <= 0) break; 23 //tickets.wait(); tickets.notifyAll(); 24 try { 25 TimeUnit.MILLISECONDS.sleep(10); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 30 System.out.println("销售了--" + tickets.remove(0)); 31 } 32 } 33 }).start(); 34 } 35 } 36 }
17.2.使用并发容器
1 public class TicketSeller4 { 2 static Queue<String> tickets = new ConcurrentLinkedQueue<>(); 3 4 static { 5 for (int i = 0; i < 1000; i++) { 6 tickets.add("票 编号:" + i); 7 } 8 } 9 10 public static void main(String[] args) { 11 12 for (int i = 0; i < 10; i++) { 13 new Thread(() -> { 14 while (true) { 15 // 判断与删除未分离,queue取不到值时值为null 16 String s = tickets.poll(); 17 if (s == null) { 18 break; 19 } else { 20 System.out.println("销售了--" + s); 21 } 22 } 23 }).start(); 24 } 25 } 26 }
18.并发容器
1 Map 2 * ConcurrentHashMap // 线程安全,比hashtable速度快,因为锁的粒度比hashtable小,hashtable锁整个hashtable,concurrentHashMap锁定的是桶 3 * ConcurrentSkipListMap // 跳表,保证线程安全的前提下排序 4 * Hashtable // 线程安全,锁的粒度大 5 * HashMap // 线程不安全,若想要线程安全,可以使用Connections.synchronziedXXX方法 6 7 CopyOnWriteArrayList写时复制 8 * CopyOnWriteArrayList写时复制 应用场景:绝大多数操作都是读操作 9 * 在add()的时候是将当前数组复制一份,将新加的数据插到最末尾,然后将指针指向复制后的数据。线程安全。 10 11 * Queue分为两种 12 * ConcurrentLinkedQueue --无界队列 13 * BlockingQueue --阻塞队列 14 15 * Queue<String> queue = new ConcurrentLinkedQueue<>() 16 * queue.offer() //返回值为boolean类型,不会报错。 17 * queue.poll() //取出一个,返回 18 * queue.peek() //返回第一个,不取出 19 20 * BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(); 21 * BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10); 22 * blockingQueue.put() // 如果满了会等待 23 * blockingQueue.take() // 如果为空会等待 24 * DelayQueue // 创建对象时会传入时间,等待时间长的对象会先被取出 25 * LinkedTransgerQueue // 生产者会先分配任务给消费者 26 * SynchronousQueue // 队列长度为0的LinkedTransferQueue