线程和进程 进程是资源分配的最小单位,线程是CPU调度的最小单位 做个简单的比喻:
进程=火车,线程=车厢线程在进程下行进(单纯的车厢无法运行)
一个进程可以包含多个线程(一辆火车可以有多个车厢)
不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-”互斥锁”
进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
创建多线程 方法一:继承Thread类 1 创建一个继承Thread类的子类
2 重写Thread类的run方法
3 创建该类的对象
4 通过该对象调用start方法
我们来写一个遍历0~100的数的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.xzc;class MyThread extends Thread { @Override public void run () { for (int i = 0 ; i < 100 ; i++) { if (i % 2 == 0 ){ System.out.println(i); } } } }public class Test { public static void main (String[] args) { MyThread t1 = new MyThread(); t1.start(); for (int i = 0 ; i < 100 ; i++) { if (i % 2 == 1 ){ System.out.println(i); } } } }
输出结果
可以看到在main线程输出到43的时候,我们输出偶数的线程开始输出,当然,每次执行结果可能会不一样。start方法有两个作用:1.启动当前线程。2.调用当前线程的run方法。 注意两个问题: 1.我们不能通过run方法去执行,因为这样不会启动线程。 2.我们不能让一个已经start的对象再次start,会报异常。
线程有关方法 void start() 启动线程,并执行对象的run方法。
void run() 线程被执行时进行的操作。
string getName() 返回线程的名称。
void setName(string name) 设置线程的名称。
static Thread currentThread() 返回当前线程。在Thread子类中就是this,通常用于主线程和runnable实现类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.xzc;class MyThread extends Thread { @Override public void run () { for (int i = 0 ; i < 10 ; i++) { if (i % 2 == 0 ){ System.out.println(Thread.currentThread().getName()+":" +i); } } } }public class Test { public static void main (String[] args) { MyThread t1 = new MyThread(); t1.setName("Thread One" ); t1.start(); Thread.currentThread().setName("Thread Main" ); for (int i = 0 ; i < 10 ; i++) { if (i % 2 == 1 ){ System.out.println(Thread.currentThread().getName()+":" +i); } } } }
输出结果:
Thread Main:1 Thread One:0 Thread Main:3 Thread Main:5 Thread Main:7 Thread One:2 Thread Main:9 Thread One:4 Thread One:6 Thread One:8
我们也可以通过构造器去起名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.xzc;class MyThread extends Thread { public MyThread () { } public MyThread (String name) { super (name); } @Override public void run () { for (int i = 0 ; i < 10 ; i++) { if (i % 2 == 0 ){ System.out.println(Thread.currentThread().getName()+":" +i); } } } }public class Test { public static void main (String[] args) { MyThread t1 = new MyThread("Thread One" ); t1.start(); Thread.currentThread().setName("Thread Main" ); for (int i = 0 ; i < 10 ; i++) { if (i % 2 == 1 ){ System.out.println(Thread.currentThread().getName()+":" +i); } } } }
输出结果:
Thread One:0 Thread Main:1 Thread One:2 Thread Main:3 Thread One:4 Thread Main:5 Thread One:6 Thread Main:7 Thread One:8 Thread Main:9
static void yield() 释放cpu的执行权。释放之后,执行权可能会被另一个线程所拿到。
void join() 在线程A中调用线程B的join方法,此时线程A就进入阻塞状态,直到线程B执行完成,线程A才可以执行。此方法会抛出异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.xzc;class MyThread extends Thread { public MyThread () { } public MyThread (String name) { super (name); } @Override public void run () { for (int i = 0 ; i < 50 ; i++) { if (i % 2 == 0 ){ System.out.println(Thread.currentThread().getName()+":" +i); } } } }public class Test { public static void main (String[] args) { MyThread t1 = new MyThread("Thread One" ); t1.start(); Thread.currentThread().setName("Thread Main" ); for (int i = 0 ; i < 50 ; i++) { if (i % 2 == 1 ){ System.out.println(Thread.currentThread().getName()+":" +i); } if (i == 19 ){ try { t1.join(); }catch (InterruptedException e){ e.printStackTrace(); } } } } }
我们这里分别两个线程输出0~50的偶数和奇数,看到设置main线程执行到19的时候,调用t1的join方法,之后一直执行t1,直到t1结束,才执行了main线程。
static void sleep(long millis) 线程休眠millis毫秒。当前线程进入阻塞状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.xzc;class MyThread extends Thread { public MyThread () { } public MyThread (String name) { super (name); } @Override public void run () { for (int i = 0 ; i < 50 ; i++) { if (i % 2 == 0 ){ try { sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":" +i); } } } }public class Test { public static void main (String[] args) { MyThread t1 = new MyThread("Thread One" ); t1.start(); Thread.currentThread().setName("Thread Main" ); for (int i = 0 ; i < 50 ; i++) { if (i % 2 == 1 ){ System.out.println(Thread.currentThread().getName()+":" +i); } if (i == 19 ){ try { t1.join(); }catch (InterruptedException e){ e.printStackTrace(); } } } } }
我们这里设置线程1,是偶数的时候休眠1秒,然后主线程直接输出到19,然后t1又join进来了,然后1秒1个t1的输出,直到t1输出完,主线程才能继续执行。
boolean isAlive() 判断当前线程是否存活。
线程的调度 Java的调度方法: 1.同优先级线程组成先进先出队列,使用时间片策略。 2.对高优先级,使用优先调度的抢占式策略。
线程的优先级等级 MAX_PRIORITY:10 MIN_PRIORITY:1 NORM_PRIORITY:5
涉及的方法 getPriority():返回线程的优先级。 setPriority(int newPriority):设置线程的优先级。
说明 线程创建时继承父线程的优先级。 低优先级只是获得调度的概率低,不一定真的比高优先级调用后再调用。
从卖票入手 先看多线程的一个经典例子:创建三个窗口卖票,总票数是100张。 先看这个代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.xzc;class Window extends Thread { public Window () { } public Window (String name) { super (name); } private int ticket = 100 ; @Override public void run () { while (true ){ if (ticket > 0 ){ System.out.println(getName()+":卖票,票号为:" +ticket--); }else { break ; } } } }public class Test { public static void main (String[] args) { Window w1 = new Window("窗口一" ); Window w2 = new Window("窗口二" ); Window w3 = new Window("窗口三" ); w1.start(); w2.start(); w3.start(); } }
输出结果: 出现了同一张票,被多个窗口卖出 的情况。这是因为我们提供的ticket是实例变量,属于每一个对象,每一个线程,这个资源不被共享。如果我们把ticket设为静态变量:
java private static int ticket = 100; 输出结果: 大致是共享了资源,但是还会出现一些问题!实际上这里就有线程安全问题。这里输出是因为要有一定延迟,所以从输出结果看并不是依次递减。
创建多线程 方法二:实现Runnable接口 1.创建一个实现了Runnable接口的类 2.实现Runnable类的抽象方法:run() 3.创建实现类的对象 4.将此对象作为参数 传递到Thread类的构造器,创建Thread类的对象 5.通过Thread类对象调用start方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.xzc;class MyThread implements Runnable { @Override public void run () { for (int i = 0 ; i < 100 ; i++) { if (i % 2 == 0 ){ System.out.println(Thread.currentThread().getName() + ":" + i); } } } }public class Test { public static void main (String[] args) { new Thread(new MyThread(),"线程一" ).start(); } }
输出结果: 遍历了0~100的偶数,这里就是创建一个实现了runnable类接口的类的对象,把他作为参数传入Thread类的构造器,其实该构造器还可以有一个参数,就是线程名,我们这里直接用。这里的start有两个作用:1.启动线程。2.调用Runnable类型的target的run方法 。我们可以重用这个类,再启动一个线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.xzc;class MyThread implements Runnable { @Override public void run () { for (int i = 0 ; i < 100 ; i++) { if (i % 2 == 0 ){ System.out.println(Thread.currentThread().getName() + ":" + i); } } } }public class Test { public static void main (String[] args) { new Thread(new MyThread(),"线程一" ).start(); new Thread(new MyThread(),"线程二" ).start(); } }
输出结果:
再谈卖票问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.xzc;class Window implements Runnable { public Window () {} private int ticket; public Window (int ticket) { this .ticket = ticket; } @Override public void run () { while (true ){ if (ticket > 0 ){ System.out.println(Thread.currentThread().getName()+"卖票,票号为:" +ticket--); }else { break ; } } } }public class Test { public static void main (String[] args) { Window w = new Window(100 ); new Thread(w,"窗口一" ).start(); new Thread(w,"窗口二" ).start(); new Thread(w,"窗口三" ).start(); } }
输出结果: 可以发现,已经能比较好地实现资源共享了,但是这种实现方式也有线程安全问题,也会有重票的问题。但是,我们没有加static 关键字,就有100张票,这是因为虽然ticket是实例变量,但是我们只有一个window对象放到构造器当参数,自然而然就是同一个ticket。
两种创建多线程方式 开发中,优先选择实现Runnable接口的方式,原因: 1.实现的方式没有类的单继承的局限性,可以让类继承一些特定的父类。 2.实现的方式更适合来处理多个线程共享数据的情况。 联系:Thread本身就实现了Runnable接口。 相同点:两种方式都要重写run方法。
线程的生命周期
线程安全 如果我们让实现runnable接口的车票程序的run方法里加个sleep
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.xzc;class Window implements Runnable { public Window () {} private int ticket; public Window (int ticket) { this .ticket = ticket; } @Override public void run () { while (true ){ if (ticket > 0 ){ try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"卖票,票号为:" +ticket--); }else { break ; } } } }public class Test { public static void main (String[] args) { Window w = new Window(100 ); new Thread(w,"窗口一" ).start(); new Thread(w,"窗口二" ).start(); new Thread(w,"窗口三" ).start(); } }
输出结果: 输出0和-1的概率大大提升。这是因为有一张票的时候,我们t1线程进来,被阻塞,还没输出,t2进来,也被阻塞,同理t3,然后就出现了如下情况: 不管是重票还是错票,问题出现的原因都是当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。如何解决?当一个线程在操作ticket的时候,其他线程不能参与进来,直到该线程完成操作。这种情况,即使该线程被阻塞,也不能被改变。
同步代码块解决实现Runnable接口的线程安全问题 关键字:synchronized
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.xzc;class Window implements Runnable { public Window () {} private int ticket; public Window (int ticket) { this .ticket = ticket; } Object obj = new Object(); @Override public void run () { while (true ){ synchronized (obj) { if (ticket > 0 ) { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket--); } else { break ; } } } } }public class Test { public static void main (String[] args) { Window w = new Window(100 ); new Thread(w,"窗口一" ).start(); new Thread(w,"窗口二" ).start(); new Thread(w,"窗口三" ).start(); } }
输出结果: 一定注意要求多个线程,共用同一把锁 ,这里我们一个window对象,就一个对象obj,保证了所有线程共用一个锁。不会出现线程安全问题。 同步的好处时解决了线程安全问题,但是操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程过程,效率比较低。
同步代码块解决实现继承Thread类的线程安全问题 因为此时是不一样的window对象,如果和之前用一样的方法,肯定不能共用一个锁,因为每一个window对象的obj实例不一样,导致无法共用一个锁。把obj设为static即可 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.xzc;class Window2 extends Thread { public Window2 () { } public Window2 (String name) { super (name); } private static int ticket = 100 ; private static final Object obj = new Object(); @Override public void run () { while (true ){ synchronized (obj) { if (ticket > 0 ) { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为:" + ticket--); } else { break ; } } } } }public class Test2 { public static void main (String[] args) { Window2 w1 = new Window2("窗口一" ); Window2 w2 = new Window2("窗口二" ); Window2 w3 = new Window2("窗口三" ); w1.start(); w2.start(); w3.start(); } }
输出结果: 在实现Runnable接口的锁,其实直接用this关键字即可。synchronized (this){},因为这里this指的是Window的一个对象,他是实现Runnable接口的类的对象,唯一,所以为了方便,不用new一个object,直接用this即可。但是在继承thread类的对象中,就不能用this了,因为我们new了三个对象出来,不是唯一的。有什么简便方法呢,用反射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.xzc;class Window2 extends Thread { public Window2 () { } public Window2 (String name) { super (name); } private static int ticket = 100 ; private static final Object obj = new Object(); @Override public void run () { while (true ){ synchronized (Window2.class) { if (ticket > 0 ) { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为:" + ticket--); } else { break ; } } } } }public class Test2 { public static void main (String[] args) { Window2 w1 = new Window2("窗口一" ); Window2 w2 = new Window2("窗口二" ); Window2 w3 = new Window2("窗口三" ); w1.start(); w2.start(); w3.start(); } }
我们在synchronized参数传入Window2的Class对象即可。synchronized (Window2.class)
同步方法解决实现Runnable的线程安全问题 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.xzc;class Window3 implements Runnable { public Window3 () { } private int ticket; public Window3 (int ticket) { this .ticket = ticket; } @Override public void run () { while (true ) { show(); } } private synchronized void show () { if (ticket > 0 ) { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket--); } } }public class Test3 { public static void main (String[] args) { Window3 w = new Window3(100 ); new Thread(w, "窗口一" ).start(); new Thread(w, "窗口二" ).start(); new Thread(w, "窗口三" ).start(); } }
输出结果: 注意这里:private synchronized void show()就是同步方法处理。 这里的锁没有显示出来,但是也是有锁,在show方法里,锁就是this 。
同步方法解决继承Thread的线程安全问题 此时不能像之前一样,直接把show方法定义为synchronized的,因为锁的不唯一 。如果非想用同步方法解决,就要在synchronized前面加一个static 声明为静态方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.xzc;class Window4 extends Thread { public Window4 () { } public Window4 (String name) { super (name); } private static int ticket = 100 ; @Override public void run () { while (true ) { show(); } } private static synchronized void show () { if (ticket > 0 ) { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket--); } } }public class Test4 { public static void main (String[] args) { Window4 w1 = new Window4("窗口一" ); Window4 w2 = new Window4("窗口二" ); Window4 w3 = new Window4("窗口三" ); w1.start(); w2.start(); w3.start(); } }
输出结果: 此时,我们的锁是当前类 ,Window4.class
同步方法总结 同步方法仍然涉及到锁,只是不需要我们显示声明 非静态的同步方法,同步监视器是this 静态的同步方法,同步监视器是当前类本身(即Class类对象) 线程安全的单例模式之懒汉式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package demo01;public class BankTest { }class Bank { private Bank () {} private static Bank instance = null ; public static Bank getInstance () { if (instance == null ){ instance = new Bank(); } return instance; } }
这样是不线程安全的,如果我们有多个线程run方法调用getInstance方法,其中一个线程进来,首次instance肯定是null,然后刚进if语句,可能会被阻塞,即使不被阻塞,cpu也可能切换到线程2,另外一个线程进来,那么instance会先后两次赋值,显然是不对的,因为instance是静态Bank类型变量。instance相当于是共享数据了 。下面用同步方式修改为线程安全的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package demo01;public class BankTest { }class Bank { private Bank () {} private static Bank instance = null ; public static Bank getInstance () { if (instance == null ){ synchronized (Bank.class){ if (instance == null ) { instance = new Bank(); } } } return instance; } }
我们先看方式一,return如果在synchronized同步代码块里面,为什么效率会低,这是因为不管instance是不是null,都进入同步代码块,如果线程非常多,就会产生效率低下问题,假设线程一拿到锁,产生一个instance返回,后面的所有线程还要在同步代码块前面等着,事实上这是无意义的,因为我们已经有instance了,后面的线程拿着instance返回就好了。而方式二就修正了这个问题,我们在同步代码块面前先判断instance是不是空,并且把return instance提出来,这样线程一二三可能在同步代码块中抢一下锁,卡一会,但是后面的所有线程进来,直接就判断instance不是null了,然后直接返回instance就好了。 这样我们就把单例模式的懒汉式修改为线程安全的了。
线程死锁 当我们有两个锁嵌套的时候,就比较容易出现死锁问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package demo01;public class ThreadTest { public static void main (String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); new Thread() { @Override public void run () { synchronized (s1) { s1.append("a" ); s2.append("1" ); synchronized (s2) { s1.append("b" ); s2.append("2" ); System.out.println(s1); System.out.println(s2); } } } }.start(); new Thread(() -> { synchronized (s2) { s1.append("c" ); s2.append("3" ); synchronized (s1) { s1.append("d" ); s2.append("4" ); System.out.println(s1); System.out.println(s2); } } }).start(); } }
输出结果 这段代码其实就有死锁隐患,这是线程顺次执行完了,我们用sleep阻塞一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package demo01;public class ThreadTest { public static void main (String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); new Thread() { @Override public void run () { synchronized (s1) { s1.append("a" ); s2.append("1" ); try { sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2) { s1.append("b" ); s2.append("2" ); System.out.println(s1); System.out.println(s2); } } } }.start(); new Thread(() -> { synchronized (s2) { s1.append("c" ); s2.append("3" ); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1) { s1.append("d" ); s2.append("4" ); System.out.println(s1); System.out.println(s2); } } }).start(); } }
当我们在嵌套的分解处加入sleep进行阻塞后,就出现了死锁问题,这段代码执行起来,就和死循环一样,不会终止,但是也不会报错,这是因为我们第一个线程执行,拿到s1这把锁,执行sleep阻塞,这时候第二个线程很大概率也被调度,拿到s2这把锁,然后执行sleep阻塞,这时候第一个线程手里有s1这把锁,醒过来后发现没有s2这把锁,没法进去下面的同步代码段,这是因为s2被第二个线程拿了,同理s2醒过来后也拿不到s1,就僵持住了,发生了死锁。 也就是发生了:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成死锁。
Lock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package demo01;import java.util.concurrent.locks.ReentrantLock;class Window implements Runnable { private int ticket = 100 ; private ReentrantLock lock = new ReentrantLock(); @Override public void run () { while (true ){ try { lock.lock(); if (ticket > 0 ){ try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"售票,票号为:" +ticket--); } else { break ; } }finally { lock.unlock(); } } } }public class LockTest { public static void main (String[] args) { Window w = new Window(); new Thread(w,"Window - 1" ).start(); new Thread(w,"Window - 2" ).start(); new Thread(w,"Window - 3" ).start(); } }
输出结果: 可以看到我们使用Reentrantlock类可以实现同步的方式,但是需要我们加锁解锁,synchronized是自动执行,加锁解锁一般就如上使用try-finally执行,一般就是先定义Reentrantlock对象,然后try里面锁住,finally里面解锁。Reentrantlock构造器还可以有一个参数:boolean fair ,如果为true,则实现公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package demo01;import java.util.concurrent.locks.ReentrantLock;class Window implements Runnable { private int ticket = 100 ; private ReentrantLock lock = new ReentrantLock(true ); @Override public void run () { while (true ){ try { lock.lock(); if (ticket > 0 ){ try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"售票,票号为:" +ticket--); } else { break ; } }finally { lock.unlock(); } } } }public class LockTest { public static void main (String[] args) { Window w = new Window(); new Thread(w,"Window - 1" ).start(); new Thread(w,"Window - 2" ).start(); new Thread(w,"Window - 3" ).start(); } }
这里我们用了公平锁,结果: 我们从这可以看到,如果三个线程一开始的等待时间确定了,后面一定是一样的,1进拿锁,3进,2进则3一定等的比2长,3拿锁,1执行完了后2一定比1等的长,2拿锁,后面同理,所以公平锁可以看做先进先出。
synchronized与Lock的异同
(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。
ReentrantLock好像比synchronized关键字没好太多,我们再去看看synchronized所没有的,一个最主要的就是ReentrantLock还可以实现公平锁机制。什么叫公平锁呢?也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
一般优先顺序(实际上都一样):Lock->同步代码块->同步方法
练习
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package demo01;import java.util.concurrent.locks.ReentrantLock;class User implements Runnable { private int money; public User () { } public User (int initMoney) { money = initMoney; } private final ReentrantLock lock = new ReentrantLock(); @Override public void run () { for (int i = 0 ; i < 3 ; i++) { try { lock.lock(); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } money += 1000 ; System.out.println(Thread.currentThread().getName() + "存了1000元," + "现有资金:" + money); } finally { lock.unlock(); } } } }public class Test { public static void main (String[] args) { User u = new User(0 ); new Thread(u, "Human - 1" ).start(); new Thread(u, "Human - 2" ).start(); } }
输出结果: 这里注意我们用的lock方式,因为采用实现runnable接口的方式来多线程,所以我们的锁是同一个,如果我们采用继承thread类方式,我们要把lock设为static的,以保证锁的唯一。
线程的通信
例题:使用两个线程打印1-100。线程和线程2交替打印。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package demo02;class Number implements Runnable { private int number = 1 ; @Override public void run () { while (true ){ synchronized (this ) { notify(); if (number <= 100 ){ System.out.println(Thread.currentThread().getName()+":" +number++); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { break ; } } } } }public class Communication { public static void main (String[] args) { Number number = new Number(); new Thread(number,"线程1" ).start(); new Thread(number,"线程2" ).start(); } }
输出结果: 这里我们用了wait和notify两个方法进行线程通信,注意notify是按优先级唤醒某个线程,notifyAll是唤醒所有线程,在这里我们只有两个线程,一个线程执行notify和notifyAll效果是一样的。而调用wait方法得到线程进入阻塞状态,一旦执行wait,会释放锁,这里和sleep不一样,sleep不会释放锁 。所以逻辑就很清晰了,线程1先进来拿到锁,唤醒2(这里其实刚开始2没有wait),然后打印1,1进入wait状态,释放锁,线程2拿到锁,进来,唤醒1,1没有锁也进不来,线程2打印2,然后进入wait,周而复始。wait,notify,notifyAll三个方法只能够出现在同步代码块或同步方法中,用lock都不行 这三个方法的调用者,必须是同步代码块或同步方法中的同步监视器,也即锁,所以用lock实现锁不能用这三个方法 所以我们上述代码中的wait();也就是this.wait();,因为锁用了this,而this又可以省略,如果用了别的锁,则不能省略。这三个方法,是定义在java.lang.Object中的,所以任意对象可以当锁,可以调用wait等方法。
sleep和wait的异同 一旦执行方法,都会使得当前的线程进入阻塞 状态。 区别:1、来自不同的类:sleep是Thread的静态类方法,wait是Object类的方法。 2、有没有释放锁(释放资源):sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。 3、sleep可以在任何需要的场景下调用,而wait方法必须在同步代码块或同步方法中使用。 4、一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。 5、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
经典例题:生产者/消费者问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 package demo02;class Producer implements Runnable { private Clerk clerk; public Producer () {} public Producer (Clerk clerk) { this .clerk = clerk; } @Override public void run () { System.out.println(Thread.currentThread().getName()+"开始生产产品……" ); while (true ){ try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } clerk.produceProduct(); } } }class Consumer implements Runnable { private Clerk clerk; public Consumer () {} public Consumer (Clerk clerk) { this .clerk = clerk; } @Override public void run () { System.out.println(Thread.currentThread().getName()+"开始消费产品……" ); while (true ){ try { Thread.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumeProduct(); } } }class Clerk { public Clerk () {} public Clerk (int productCount) { this .productCount = productCount; } private int productCount; public synchronized void produceProduct () { if (productCount < 20 ){ productCount++; System.out.println(Thread.currentThread().getName()+":开始生产第" +productCount+"个产品" ); notify(); }else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void consumeProduct () { if (productCount > 0 ){ System.out.println(Thread.currentThread().getName()+":开始消费第" +productCount+"个产品" ); productCount--; notify(); }else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }public class Product { public static void main (String[] args) { Clerk clerk = new Clerk(0 ); Producer p1 = new Producer(clerk); Thread t1 = new Thread(p1,"生产者1" ); Consumer c1 = new Consumer(clerk); Consumer c2 = new Consumer(clerk); Thread t2 = new Thread(c1,"消费者1" ); Thread t3 = new Thread(c2,"消费者2" ); t1.start(); t2.start(); t3.start(); } }
执行结果:
JDK5.0新增线程创建方式 实现Callable接口
FutureTask同时实现Runnable接口和Future接口,所以可以放进Thread类构造器。 1.创建一个实现Callable的实现类。 2.覆写call方法,将此线程需要执行的操作写在call中,注意有返回值。 3.创建Callable接口实现类的对象。 4.将此Callable接口实现类的对象作为参数传递到FutureTask的构造器,创建FutureTask的对象。 5.将该FutureTask对象传入Thread构造器,创建Thread对象,并调用start方法。 如果对call返回值感兴趣,调用get方法获取返回值。 如何理解实现Callable接口的方式比实现Runnable接口方式更强大? 1.call可以有返回值。 2.call可以抛出异常,被外部捕获,获取异常的信息。 3.Callable支持泛型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package demo02;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;class NumThread implements Callable { @Override public Object call () throws Exception { int sum = 0 ; for (int i = 1 ; i <= 10 ; i++) { if (i % 2 == 0 ){ sum += i; System.out.println(i); } } return sum; } }public class ThreadNew { public static void main (String[] args) { NumThread numThread = new NumThread(); FutureTask futureTask = new FutureTask(numThread); new Thread(futureTask).start(); try { Object o = futureTask.get(); System.out.println("总和为" +o); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
结果 使用泛型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package demo02;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;class NumThread implements Callable <Integer > { @Override public Integer call () throws Exception { int sum = 0 ; for (int i = 1 ; i <= 10 ; i++) { if (i % 2 == 0 ){ sum += i; System.out.println(i); } } return sum; } }public class ThreadNew { public static void main (String[] args) { NumThread numThread = new NumThread(); FutureTask<Integer> futureTask = new FutureTask<Integer>(numThread); new Thread(futureTask).start(); try { Integer o = futureTask.get(); System.out.println("总和为" +o); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
使用线程池 下面只看创建一个可重用固定线程数的线程池
java ExecutorService service = Executors.newFixedThreadPool(n); 我们声明的ExecutorService类的service有两个方法,一个是execute 方法,适用于Runnable 接口的实现类,一个是submit 方法,适用于Callable 接口实现类。执行了后,用shutdown 方法结束线程池。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package demo02;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;class NumThread1 implements Runnable { @Override public void run () { for (int i = 1 ; i <= 20 ; i++) { if (i % 2 == 1 ) { System.out.println(Thread.currentThread().getName()+":" +i); } } } }class NumThread2 implements Runnable { @Override public void run () { for (int i = 1 ; i <= 20 ; i++) { if (i % 2 == 0 ) { System.out.println(Thread.currentThread().getName()+":" +i); } } } }public class ThreadPool { public static void main (String[] args) { ExecutorService service = Executors.newFixedThreadPool(10 ); service.execute(new NumThread2()); service.execute(new NumThread1()); service.shutdown(); } }
结果: 这个过程大致如下: 1.提供指定线程数量的线程池。 2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口的类的对象。 3.关闭线程池。 开发中一般都用线程池,因为有如下好处: 线程池的管理,因为ExecutorService是接口,我们没法设置属性,因为接口有属性也是常量,所以我们要找他的实现类,通过反射可以找到为ThreadPoolExecutor ,此时我们把service强制转换为ThreadPoolExecutor类,就可以进行属性设置了
守护线程 java中的线程分为两种:守护线程(Daemon) 和用户线程(User) 。
任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。 Thread.setDaemon()必须在Thread.start()之前调用 ,否则运行时会抛出异常。
两者的区别:
唯一的区别是判断虚拟机(JVM)何时离开 ,Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon 没有可服务的线程,JVM撤离 。也可以理解为守护线程是JVM自动创建的线程(但不一定),用户线程是程序创建的线程 ;比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开 。
优雅地停止线程 Thread类中的stop方法已经被舍弃了。 除了stop方法,还有几个方法也被禁用了:销毁多线程(destroy),挂起多线程(suspend),恢复挂起(resume),之所以废除这些方法是因为有可能导致线程死锁。 范例:实现线程柔和地停止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package demo02;public class ThreadDemo { private static boolean flag = true ; public static void main (String[] args) throws Exception { new Thread(()->{ long num = 0 ; while (flag){ try { Thread.sleep(50 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在执行,num=" +num++); } },"执行线程" ).start(); System.out.println(Thread.currentThread().getName()); Thread.sleep(200 ); flag = false ; } }
输出结果:
main 执行线程正在执行,num=0 执行线程正在执行,num=1 执行线程正在执行,num=2 执行线程正在执行,num=3
就是线程main先休眠200ms后,将flag设为false,这样“执行线程”每50ms输出一次,输出4次后run方法里的while循环就结束了,线程就停止了。万一现在有其他线程去控制这个flag的内容,那么对于这个执行线程的停止也不是说停就立刻停,而是会在执行中判断flag内容完成 。这样就能解决线程死锁。
守护线程例子 还没设守护线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package demo02;public class ThreadDemo { public static void main (String[] args) throws Exception { Thread userThread = new Thread(()->{ for (int i = 0 ; i < 10 ; i++) { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在执行,x = " + i); } },"用户线程" ); Thread daemonThread = new Thread(()->{ for (int i = 0 ; i < Integer.MAX_VALUE; i++) { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在执行,x = " + i); } },"守护线程" ); userThread.start(); daemonThread.start(); } }
设置为守护线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package demo02;public class ThreadDemo { public static void main (String[] args) throws Exception { Thread userThread = new Thread(()->{ for (int i = 0 ; i < 10 ; i++) { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在执行,x = " + i); } },"用户线程" ); Thread daemonThread = new Thread(()->{ for (int i = 0 ; i < Integer.MAX_VALUE; i++) { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在执行,x = " + i); } },"守护线程" ); daemonThread.setDaemon(true ); userThread.start(); daemonThread.start(); } }
volatile关键字 在多线程的定义中,volatile关键字主要是在属性定义上使用的,表示此属性为直接数据操作,而不进行副本的拷贝处理。在正常进行变量处理的时候往往会经历如下一个步骤: 1.获取变量原有的数据内容副本 2.利用副本为变量进行数学计算 3.将计算后的变量,保存到原始空间之中 而如果一个属性上追加了volatile关键字,表示就是不使用副本,而是直接操作原变量,相当于节约了拷贝副本,重新保存的步骤。 用了volatile关键字,该加同步还是要加,只不过volatile是直接内存操作,没有拷贝与赋值环节了。
volatile和synchronized区别 volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。 volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的 volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性 volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。 volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
多线程案例 案例分析一:加减操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 package demo02;class Resource { Resource(){ } Resource(int num,boolean flag){ this .num = num; this .flag = flag; } private int num; private boolean flag; public synchronized void inc () throws InterruptedException { if (!flag){ wait(); } this .num++; System.out.println("[加法操作]->num:" +this .num); flag = false ; notifyAll(); } public synchronized void sub () throws InterruptedException { if (flag){ wait(); } this .num--; System.out.println("[减法操作]->num:" +this .num); flag = true ; notifyAll(); } }class IncThread implements Runnable { Resource r; IncThread(){} IncThread(Resource r){ this .r = r; } @Override public void run () { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0 ; i < 50 ; i++) { try { r.inc(); } catch (InterruptedException e) { e.printStackTrace(); } } } }class SubThread implements Runnable { Resource r; SubThread(){} SubThread(Resource r){ this .r = r; } @Override public void run () { try { Thread.sleep(200 ); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0 ; i < 50 ; i++) { try { r.sub(); } catch (InterruptedException e) { e.printStackTrace(); } } } }public class TestOne { public static void main (String[] args) { Resource r = new Resource(0 ,true ); IncThread i1 = new IncThread(r); IncThread i2 = new IncThread(r); SubThread s1 = new SubThread(r); SubThread s2 = new SubThread(r); new Thread(i1,"加法线程1" ).start(); new Thread(i2,"加法线程2" ).start(); new Thread(s1,"减法线程1" ).start(); new Thread(s2,"减法线程2" ).start(); } }
这也是一个很经典的多线程问题,主要是要考虑到加减要交错,该加的时候就不能减,要控制方向,用一个flag实现线程的通信。
案例分析二:生产电脑 设计一个生产电脑和搬运电脑类,要求生产出一台电脑就搬走一台电脑,如果没有新的电脑生产出来,则搬运工需要等待新电脑生产出,如果生产出的电脑没有搬走,则要等待电脑搬走后再生产,并统计电脑数量。本质还是生产者消费者模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 package demo02;class Produce implements Runnable { Res r; Produce(Res r){ this .r = r; } @Override public void run () { for (int i = 0 ; i < 50 ; i++) { try { r.make(); System.out.println(Thread.currentThread().getName()+"生产了一台电脑" ); } catch (Exception e) { e.printStackTrace(); } } } }class Get implements Runnable { Res r; Get(Res r){ this .r = r; } @Override public void run () { for (int i = 0 ; i < 50 ; i++) { try { r.fetch(); System.out.println(Thread.currentThread().getName()+"搬走了一台电脑" ); } catch (Exception e) { e.printStackTrace(); } } } }class Res { private Computer computer; public synchronized void make () throws Exception { if (this .computer != null ){ wait(); } Thread.sleep(200 ); this .computer = new Computer("HP" ,8000 ); System.out.println(this .computer); notifyAll(); } public synchronized void fetch () throws Exception { if (this .computer == null ){ wait(); } Thread.sleep(100 ); this .computer = null ; notifyAll(); } }class Computer { private static int count = 0 ; private String name; private double price; public Computer (String name,double price) { this .name = name; this .price = price; count++; } @Override public String toString () { return "第" +count+"台电脑{" + "name='" + name + '\'' + ", price=" + price + '}' ; } }public class TestTwo { public static void main (String[] args) { Res r = new Res(); Produce p = new Produce(r); Get g = new Get(r); new Thread(p,"生产者" ).start(); new Thread(g,"搬运工" ).start(); } }
案例分析三:竞争抢答 实现一个竞拍抢答程序,设计三个抢答者,同时发出抢答指令,抢答成功者给出成功提示,未能抢答成功给出失败提示。 对于这个多线程牵扯到数据返回问题,那么使用实现Callable 接口的方式最好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package demo02;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;class MyThread1 implements Callable <String > { private boolean flag = false ; @Override public String call () throws Exception { synchronized (this ){ if (!flag){ flag = true ; return Thread.currentThread().getName()+"抢答成功" ; }else { return Thread.currentThread().getName()+"抢答失败" ; } } } }public class TestThree { public static void main (String[] args) throws ExecutionException, InterruptedException { MyThread1 m = new MyThread1(); FutureTask<String> futureTask1 = new FutureTask<String>(m); FutureTask<String> futureTask2 = new FutureTask<String>(m); FutureTask<String> futureTask3 = new FutureTask<String>(m); new Thread(futureTask1,"抢答者1" ).start(); new Thread(futureTask2,"抢答者2" ).start(); new Thread(futureTask3,"抢答者3" ).start(); System.out.println(futureTask1.get()); System.out.println(futureTask2.get()); System.out.println(futureTask3.get()); } }