21天学会Java之(Java SE第十二篇):多线程、Lambda表达式
多线程是Java语言的重要特性,大量应用于网络编程和服务器端程序的开发。最常见的UI界面的底层原理、操作系统底层原理都大量使用了多线程技术。本篇中仅初步讲解多线程的普通应用,并无深入剖析。由于JUC包的内容过多,过于深奥,本人水平有限,本文中也不扩展叙写,希望在对于并发编程有更深一步的理解之后填上这个坑。
多线程的基本概念
对于线程的理解,我们需要先理解程序、进程以及线程的概念。
程序是一个静态的概念,一般对应于操作系统中的一个可执行文件,例如,打开用于敲代码的idea的可执行文件。打开idea可执行文件,将会加载该程序到内存中并开始执行它,于是就产生了“进程”,而我们打开了多个可执行文件,这就产生了多个进程。
对于多任务,多进程大多数人应该就特别熟悉,我们打开电脑上的任务管理器/活动监视器,我们就能看到一大堆进程,这是操作系统的一种能力,看起来可以在同一时刻运行多个程序。例如,我们在敲代码的时候能同时用音乐软件听歌。而如今,人们往往都有多CPU多计算机,但是并发执行的进程数目并不受限于CPU数目。操作系统会为每个进程分配CPU的时间片,给人并行处理的感觉。
多线程程序在更低一层扩展了多任务多概念:单个程序看起来在同时完成多个任务。每个任务在一个线程中执行,线程是控制线程的简称。如果一个程序可以同时运行多个线程,则称这个程序是多线程的程序。
而多线程和多进程的本质区别在于每个进程都拥有自己的一套变量,而线程则共享数据。而这样就会涉及线程安全的问题,下文会介绍这个问题。不过对于共享变量使线程之间的通信比进程之间的通信更有效、更容易。此外,在操作系统中,与进程相比较,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小得多,所以线程又被称为轻量级进程。
Java中如何实现多线程
Java中使用多线程非常的简单。下文将会介绍如何创建和使用线程。
通过继承Thread类实现多线程
继承Thread类实现多线程的步骤如下:
- 在Java中负责实现线程功能的类是java.lang.Thread类。
- 可以通过创建Thread的实例来创建新的线程。
- 每个线程都是通过某个特定的Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。
- 通过调用Thread类的start()方法来启动一个线程。
可以参考以下代码理解:
/** * 创建线程的方式一: * 1.创建:继承Thread并且重写run方法 * 2.启动:创建子类对象并且运行start方法 * @author Eddie * */ public class StartThread extends Thread { //程序入口点 @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("一边听歌......"); } } public static void main(String[] args) { //创建子类对象 StartThread st = new StartThread(); //启动线程 st.start(); //不保证立即运行,靠cpu调用 // st.run(); //仅调用普通的run方法 for (int i = 0; i < 20; i++) { System.out.println("一边敲代码......"); } } }
这种方法的缺点是:如果类已经继承一个类,则无法继承Thread类(Java只能继承一个父类)。
通过Runnable接口实现多线程
在实际开发中,更多的是通过Runnable接口实现的多线程。这种方式完美解决了继承Thread类的缺点,在实现Runnable接口的同时还可以继承某个类。所以实现Runnable接口的方式要通用一些。
可以参考以下代码理解:
/** * 创建线程的方式二: * 1.创建:实现Runnable并且重写run方法 * 2.启动:创建实现类对象和Thread对象并且运行start方法 * 推荐:避免单继承的局限性,优先使用接口 * 方便共享资源 * @author Eddie * */ public class StartRunnable implements Runnable { //线程入口点 @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("一边听歌......"); } } public static void main(String[] args) { // //创建子类对象 // StartRunnable sr = new StartRunnable(); // Thread t=new Thread(sr); // //启动线程 // t.start(); //不保证立即运行,靠cpu调用 // st.run(); //仅调用普通的run方法 new Thread(new StartRunnable()).start(); //同样可以使用匿名对象的方式来使用子类 for (int i = 0; i < 20; i++) { System.out.println("一边敲代码......"); } } }
线程状态和生命周期
线程状态
一个成对象在它的生命周期内,需要经历5个状态,如下图所示:
新生状态
用new关键字建立一个线程对象后,该线程对象处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。
就绪状态
处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得哦CPU,线程就进入运行状态并自动调用其run方法。下列4种原因会导致线程进入就绪状态:
- 新建线程:调用start()方法,进入就绪状态。
- 阻塞线程:阻塞解除,进入就绪状态。
- 运行线程:调用yield()方法,直接进入就绪状态。
- 运行线程:JVM将CPU资源从本线程切换到其他线程。
运行状态
在运行状态的线程执行其run方法中的代码,直到因调用其他方法而终止,或等待某资源产生阻塞或完成任务死亡。如果在给定的时间片内没有执行结束,线程就会被系统换下来并回到就绪状态,也可能由于某些“导致阻塞的事件”而进入阻塞状态。
阻塞状态
阻塞是指暂停一个线程的执行以等待某个条件发生(如其资源就绪)。有4种原因会导致阻塞:
- 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了之后,线程进入就绪状态。
- 执行wait()方法,使当前线程进入阻塞状态。当使用notify()方法唤醒这个线程后,它进入就绪状态。
- 当线程运行时,某个操作进入阻塞状态,例如执行I/O流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程才进入就绪状态。
- join()线程联合:当某个线程等待另一个线程执行结束并能继续执行时,使用join()方法。
死亡状态
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个:一个是正常运行的线程完成了它run()方法内的全部工作;另外一个是线程被强制终止,如通过执行
stop()或destroy()方法来终止一个线程(stop()/destroy()方法已经被JDK废弃,不推荐使用)。当一个线程进入死亡状态以后,就不能回到其他状态了。
终止线程的常用方式
上文中提到stop()/destroy()方法已经被JDK废弃,不推荐使用。当我们需要终止线程的时候通常的做法是提供一个boolean类型的终止变量,当这个变量置为false时,终止线程的运行。可以参考以下代码:
/** * 终止线程 * 1.线程正常执行完毕/2.外部干涉,加入标识(这边所要使用的方法) * @author Eddie * */ public class TerminateThread implements Runnable{ //加入标识 标记线程体是否可以运行 private boolean flag=true; private String name; public TerminateThread() { } public TerminateThread(String name) { super(); this.name = name; } public String getName() { return name; } @Override public void run() { int i=0; //关联标识 while (flag) { System.out.println(name+"运行:"+(i++)+"次。"); } } //对外提供改变标识的方法。 public void stop() { this.flag=false; } public static void main(String[] args) { TerminateThread tt = new TerminateThread("线程"); //新生状态 new Thread(tt).start(); //就绪状态 for (int i = 0; i < 99; i++) { System.out.println("主线程运行了:"+i+"次。"); if (i==66) { System.out.println(tt.getName()+"STOP!"); tt.stop(); } } } }
暂停线程执行的常用方法
暂停线程的常用方法有sleep()和yield(),这两个方法的区别如下:
- sleep()方法可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。
- yield()方法可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。
sleep()方法使用的示范代码:
public class BlockedSleep { public static void main(String[] args) { StateThread t1 = new StateThread(); StateThread t2 = new StateThread(); t1.start(); t2.start(); } } //这里为了简洁实用继承的方式实现多线程 class StateThread extends Thread { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println(this.getName() + ":" + i); try { Thread.sleep(1000); //调用线程的sleep()方法 } catch (InterruptedException e) { e.printStackTrace(); } } } }
yield()方法使用的示范代码:
public class BlockedYield { public static void main(String[] args) { StateThread t1 = new StateThread(); StateThread t2 = new StateThread(); t1.start(); t2.start(); } } //这里为了简洁实用继承的方式实现多线程 class StateThread extends Thread { @Override public void run() { for (int i = 0; i < 99; i++) { System.out.println(this.getName() + ":" + i); Thread.yield(); //调用线程的yield()方法 } } }
以上代码可以自己copy进IDE运行看下运行结果,sleep()方法中我们可以感觉到每条结果输出之前的延迟,这是因为Thread.sleep(1000)语句在起作用。而在yield()方法中,代码可以引起线程的切换,但运行没有明显延迟。
联合(合并)线程的使用方法
线程A运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕,才能继续执行。用以下一个例子来说明一下join()方法的使用:
/** * join:合并线程,插队线程 * @author Eddie * */ public class BlockedJoin02 { public static void main(String[] args) { new Thread(new father()).start(); } } class father implements Runnable{ @Override public void run() { System.out.println("爸爸想抽烟了。"); System.out.println("拿钱叫儿子去买烟。"); Thread sonThread=new Thread(new son()); sonThread.start(); try { sonThread.join(); //调用join()方法 System.out.println("拿到了烟,把零钱给儿子。"); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("儿子走丢了,出门找儿子。"); } } } class son implements Runnable{ @Override public void run() { System.out.println("儿子拿了钱,出门买烟。!"); System.out.println("路过了游戏厅。"); for (int i = 0; i <= 10; i++) { System.out.println("在游戏厅里呆了"+i+"秒。"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("走出游戏厅去便利店买烟"); System.out.println("回家把烟给爸爸。"); } }
Lambda表达式
Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。本文仅简单介绍一下如何使用Lambda表达式,以及Lambda在多线程中的使用,更详细的内容可以翻阅相关的书籍。
Lambda表达式的推导
public class LambdaTest01 { //非静态内部类 class Like2 implements ILike{ @Override public void lambda() { System.out.println("I like lambda2"); } } //静态内部类 static class Like3 implements ILike{ @Override public void lambda() { System.out.println("I like lambda3"); } } public static void main(String[] args) { //外部类 ILike like=new Like1(); like.lambda(); //非静态内部类 like =new LambdaTest01().new Like2(); like.lambda(); //静态内部类 like =new Like3(); like.lambda(); //局部内部类 class Like4 implements ILike{ @Override public void lambda() { System.out.println("I like lambda4"); } } like=new Like4(); like.lambda(); //匿名内部类 like=new ILike() { @Override public void lambda() { System.out.println("I like lambda5"); } }; like.lambda(); //Lambda表达式 like=()-> { System.out.println("I like lambda5"); }; like.lambda(); // Lambda推导必须存在类型 // ()-> { // System.out.println("I like lambda5"); // }.lambda(); } } //接口中只能有一个要实现的方法 interface ILike{ void lambda(); } //外部类 class Like1 implements ILike{ @Override public void lambda() { System.out.println("I like lambda1"); } }
Lambda表达式参数的简化过程
public class LambdaTest02 { public static void main(String[] args) { ILove love=(String a)-> { System.out.println("I like lambda-->"+a); }; love.lambda("普通Lambda表达式"); //可以去掉参数类型 love=(a)-> { System.out.println("I like lambda-->"+a); }; love.lambda("去掉参数类型"); //只有一个参数括号可以省略 love=a-> { System.out.println("I like lambda-->"+a); }; love.lambda("省略参数括号"); //只有一行代码可以省略花括号 love=a->System.out.println("I like lambda-->"+a); love.lambda("省略花括号"); } } interface ILove{ void lambda(String a); } //外部类 //class Love1 implements ILove{ // @Override // public void lambda(String a) { // System.out.println("I like lambda-->"+a); // } //}
Lambda表达式返回值的简化过程
public class LambdaTest03 { public static void main(String[] args) { //普通的Lambda表达式 IInsterest insterest=(int a1, int b1)-> { System.out.println("I like lambda-->"+(a1+b1)); return a1+b1; }; insterest.lambda(1, 1); //去掉参数类型(去掉的话需要全部去掉,仅去掉一个不可行) insterest=(a1, b1)-> { System.out.println("I like lambda-->"+(a1+b1)); return a1+b1; }; insterest.lambda(2, 2); /* * 有两个参数不可省略参数的括号 * 有两行代码不可省略花括号 */ //如果只有一行代码,并且有返回值可以省略return; insterest=(a1, b1)->a1+b1; //返回了一个int数值 System.out.println(insterest.lambda(6, 6)); insterest=(a1, b1)->100; //返回了一个int数值 System.out.println(insterest.lambda(100, 100)); } } interface IInsterest{ int lambda(int a,int b); } //class Insterest implements IInsterest{ // @Override // public int lambda(int a1, int b1) { // System.out.println("I like lambda-->"+(a1+b1)); // return a1+b1; // } //}
Lambda表达式简化线程(用一次)的使用
public class LambdaThread01 { //静态内部类 static class Test1 implements Runnable{ @Override public void run() { for (int i = 0; i < 2; i++) { System.out.println("一边听歌1"); } } } //非静态内部类 class Test2 implements Runnable{ @Override public void run() { for (int i = 0; i < 2; i++) { System.out.println("一边听歌2"); } } } public static void main(String[] args) { //静态内部类 new Thread(new Test1()).start(); //非静态内部类 new Thread(new LambdaThread01().new Test2()).start(); //局部内部类 class Test3 implements Runnable{ @Override public void run() { for (int i = 0; i < 2; i++) { System.out.println("一边听歌3"); } } } new Thread(new Test3()).start(); //匿名内部类 new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 2; i++) { System.out.println("一边听歌4"); } } }).start(); //jdk8简化 Lambda表达式 //因为Thread里只能传入一个实现Runable接口的实现类并且Runable仅需要实现一个run()方法 new Thread(()-> { for (int i = 0; i < 2; i++) { System.out.println("一边听歌5"); } } ).start(); for (int i = 0; i < 5; i++) { System.out.println("66666666666"); } } }
使用Lambda表达式简化多线程
/** * 使用Lambda表达式简化多线程 * Lambda表达式避免匿名内部类定义过多 * 其实质属于函数式编程的概念 * @author Eddie * */ public class LambdaThread02 { public static void main(String[] args) { new Thread(()->{ for (int i = 0; i < 10; i++) { System.out.println("一边听歌..."); } }).start(); new Thread(()->System.out.println("正在学习Lambda表达式")).start(); for (int i = 0; i < 10; i++) { System.out.println("一边写代码..."); } } }
线程的常用方法
线程也是对象,系统为线程定义了很多方法、优先级、名字等,以便对多线程进行有效地管理。
线程常用的方法
线程的常用方法如下表所示:
方法 | 功能 |
---|---|
getState() | 获得线程当前的状态 |
isAlive() | 判断线程是否还“活着”,即线程是否还未终止 |
getPriority() | 获得线程的优先级数值 |
setPriority() | 设置线程的优先级数值 |
setName() | 给线程设置一个名字 |
getName() | 获得线程的名字 |
currentThread() | 取得当前正在运行的线程对象,也就是取得自己本身 |
setDaemon(boolean on) | 将线程设置成守护线程 |
使用getState()方法观察线程状态
public class AllState { public static void main(String[] args) { Thread t=new Thread(()->{ for (int i = 0; i <5; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("模拟线程"); } }); //观察状态 State state=t.getState(); System.out.println(state); //NEW t.start(); state=t.getState(); System.out.println(state); //RUNNABLE // while (state!=State.TERMINATED) { // try { // Thread.sleep(200); // } catch (InterruptedException e) { // e.printStackTrace(); // } // state=t.getState(); // System.out.println(state); //TIMED_WAITING // } while (true) { //活动的线程数 int threadNum=Thread.activeCount(); if (threadNum==1) { break; } try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } state=t.getState(); System.out.println(state); //TIMED_WAITING } state=t.getState(); System.out.println(state); //TERMINATED } }
线程的优先级
/** * 线程的优先级1-10 * 1.NORM_PRIORITY 5 默认 * 2.MIN_PRIORITY 1 * 3.MAX_PRIORITY 10 * 概率,不代表绝对的先后顺序 * @author Eddie * */ public class PriorityTest { public static void main(String[] args) { MyPriority mp=new MyPriority(); Thread t1=new Thread(mp,"百度"); Thread t2=new Thread(mp,"阿里"); Thread t3=new Thread(mp,"腾讯"); Thread t4=new Thread(mp,"头条"); Thread t5=new Thread(mp,"美团"); Thread t6=new Thread(mp,"滴滴"); //设置优先级需要在线程启动前 t1.setPriority(Thread.MAX_PRIORITY); t2.setPriority(Thread.MAX_PRIORITY); t3.setPriority(Thread.MAX_PRIORITY); t4.setPriority(Thread.MIN_PRIORITY); t5.setPriority(Thread.MIN_PRIORITY); t6.setPriority(Thread.MIN_PRIORITY); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); t6.start(); System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority()); } } class MyPriority implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority()); Thread.yield(); } }
其他方法的示例
/** * 其他方法 * isAlive:线程是否还或者 * Thread.currentThread():当前线程 * setName.getName:设置和获取代理线程的名称 * @author Eddie * */ public class InfoTest { public static void main(String[] args) { System.out.println(Thread.currentThread().isAlive()); MyInfo myInfo=new MyInfo("战斗机"); Thread t=new Thread(myInfo); t.setName("公鸡"); t.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(t.isAlive()); } } class MyInfo implements Runnable{ private String name; public MyInfo(String name) { super(); this.name = name; } @Override public void run() { System.out.println(Thread.currentThread().getName()+"-->"+name); } }
守护线程
/** * 守护线程:是为用户线程服务的;JVM停止不用等待守护线程执行完毕 * 线程默认用户线程 JVM等待用户线程执行完毕才会停止 * @author Eddie * */ public class DaemonTest { public static void main(String[] args) { God god=new God(); You you=new You(); Thread t=new Thread(god); t.setDaemon(true); //将用户线程设置为守护线程 t.start(); new Thread(you).start(); } } class You implements Runnable{ @Override public void run() { for (int i = 1; i <=365*100; i++) { System.out.println("Happy Life"+i+"days."); } System.out.println("die..."); } } class God implements Runnable{ @Override public void run() { while (true) { System.out.println("Bless you..."); } } }
线程同步
在处理多线程问题时,如果多个线程同时访问同一个对象,并且某些线程还想修改这个对象时,就需要用到“线程同步”机制。加入线程同步后,我们称为这是线程安全的;线程安全在并发时保证数据的准确性、效率尽可能高。
线程同步的概念
线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程继续使用。
用一个取款机的例子来看下未使用线程同步的情况下会发生的情况:
public class UnsafeTest02 { public static void main(String[] args) { Account account=new Account(100, "百万账户"); ATM atm01=new ATM(account, 80); ATM atm02=new ATM(account, 70); new Thread(atm01,"自己").start(); new Thread(atm02,"老婆").start(); } } //账户 class Account{ private int total_assets; //账户总资产 private String account_name; //账户名字 public Account(int total_assets, String account_name) { super(); this.total_assets = total_assets; this.account_name = account_name; } public int getTotal_assets() { return total_assets; } public void setTotal_assets(int total_assets) { this.total_assets = total_assets; } public String getAccount_name() { return account_name; } public void setAccount_name(String account_name) { this.account_name = account_name; } } //模拟取款 class ATM implements Runnable{ private Account account; //取款账户 private int withdrawMoney; //取款金额 private int pocketMoney; //口袋的钱 public ATM(Account account, int withdrawMoney) { super(); this.account = account; this.withdrawMoney = withdrawMoney; } @Override public void run() { if (account.getTotal_assets()<withdrawMoney) { return; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } account.setTotal_assets(account.getTotal_assets()-withdrawMoney); pocketMoney+=withdrawMoney; System.out.println(Thread.currentThread().getName()+":"+pocketMoney); System.out.println(account.getAccount_name()+":"+account.getTotal_assets()); } }
由于没有使用线程同步机制,即使我们在线程中判断了剩余余额,但是同样会使两个人都取款成功,这就叫做线程不安全。
实现线程同步
由于同一进程的多个线程共享同一块存储空间,这在带来方便的同时,也带来了访问冲突问题。Java语言提供了专门机制来解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的问题。这套机制就是使用synchronized关键字,它包括两种用法:synchronized方法和synchronized块。
synchronized方法
通过在方法声明中加入synchronized关键字来声明此方法,语法格式如下:
public synchronized void accessVal(int newVal);
synchronized方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则所属线程阻塞。方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
synchronized块
synchronized方法的缺陷是,若将一个大的方法声明为synchronized将会大大影响程序的工作效率。
为此,Java提供了更好的解决办法,就是使用synchronized块。synchronized块可以让人们精确地控制具体的“成员变量”,缩小同步的范围,提高效率。且synchronized块可以指定锁的对象,synchronized方法则只能锁本对象。
通过synchronized关键字可声明synchronized块,语法格式如下:
synchronized(synObject){ //允许访问控制的代码 }
将以上取款机的例子加入线程同步:
public class SynBlock01 { public static void main(String[] args) { Account account=new Account(200, "百万账户"); SynATM my = new SynATM(account,80); SynATM wife = new SynATM(account,90); new Thread(my,"自己").start(); new Thread(wife,"妻子").start(); } } //账户 class Account{ private int total_assets; //账户总资产 private String account_name; //账户名字 public Account(int total_assets, String account_name) { super(); this.total_assets = total_assets; this.account_name = account_name; } public int getTotal_assets() { return total_assets; } public void setTotal_assets(int total_assets) { this.total_assets = total_assets; } public String getAccount_name() { return account_name; } public void setAccount_name(String account_name) { this.account_name = account_name; } } class SynATM implements Runnable{ private Account account; private int drawingMoney; private int money; public SynATM(Account account, int drawingMoney) { super(); this.account = account; this.drawingMoney = drawingMoney; } @Override public void run() { //提高性能,判断账户是否有钱或者取的钱是否超过账户余额,满足条件直接返回,不需要运行同步块 if (account.getTotal_assets()<=0 || account.getTotal_assets()<drawingMoney) { return; } //同步块:目标锁定account synchronized (account) { account.setTotal_assets(account.getTotal_assets()-drawingMoney); money+=drawingMoney; System.out.println(Thread.currentThread().getName()+"钱包余额:"+money); System.out.println(account.getAccount_name()+"余额:"+account.getTotal_assets()); } } }
synchronized (account)意味着线程需要获得account对象的“锁”才有资格运行同步块中的代码。Account对象的“锁”也称为“互斥锁”,在同一时刻只能被一个线程使用。A线程拥有锁,则可以调用“同步块”中的代码;B线程没有锁,则进入account对象的“锁池队列”等待,直到A线程使用完毕释放了account对象的锁,B线程得到锁才可以调用“同步块”中的代码。
synchronized方法、synchronized块和线程不安全的例子
以下是买票的例子:
public class SynBlock03 { public static void main(String[] args) { Syn12306 web12306 = new Syn12306(); new Thread(web12306,"黄牛").start(); new Thread(web12306,"yellow牛").start(); new Thread(web12306,"ticket_scalper").start(); } } class Syn12306 implements Runnable{ //票数 private int ticketNums=10; private boolean flag=true; @Override public void run() { while (flag) { test5(); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } } } //线程安全,范围太大-->性能效率低下:同步方法,锁定的是SynWeb对象 public synchronized void test1() { if (ticketNums<=0) { flag=false; return; } //模拟网络延迟 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } //线程安全,范围太大-->性能效率低下:同步块,锁定this对象,即SynWeb对象 public void test2() { synchronized(this) { if (ticketNums<=0) { flag=false; return; } //模拟网络延迟 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } } //线程不安全:同步块,锁定ticketNums对象的属性在变 public void test3() { synchronized((Integer)ticketNums) { if (ticketNums<=0) { flag=false; return; } //模拟网络延迟 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } } //线程不安全:同步块 public void test4() { //仅锁定下面一部分,线程不安全 synchronized(this) { if (ticketNums<=0) { flag=false; return; } } //模拟网络延迟 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } //线程安全:尽可能锁定合理的范围(不是指代码 指数据的完整性) //double checking public void test5() { if (ticketNums<=0) { //考虑的是没有票的情况 flag=false; return; } //仅锁定下面一部分,线程不安全 synchronized(this) { if (ticketNums<=0) { //考虑的是最后一张票的情况 flag=false; return; } //模拟网络延迟 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--); } } }
死锁及解决方案
死锁的概念
“死锁”指的是多个线程各自占有一些共享资源,并且互相等待得到其他线程占有的资源才能继续,从而导致两个或者多个线程都在等待对方释放资源,停止执行的情形。
因此,某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。用以下一个例子来描述下死锁的形成:
public class DeadLock { public static void main(String[] args) { new Thread(new MarkUp("大丫", true)).start(); new Thread(new MarkUp("二丫", false)).start(); } } //镜子 class Mirror{ } //口红 class Lipstick{ } //化妆 class MarkUp implements Runnable{ //不管几个对象只有一份 static Mirror mirror=new Mirror(); static Lipstick lipstick=new Lipstick(); private String girl; private boolean flag; public MarkUp(String girl, boolean flag) { this.girl = girl; this.flag = flag; } @Override public void run() { markup(); } //相互持有对方的对象锁 private void markup() { if (flag) { synchronized (mirror) { //先将镜子锁上 System.out.println(this.girl+"照镜子。"); //1秒后,涂口红 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lipstick) { //然后将口红锁上 System.out.println(this.girl+"涂口红。"); } } }else { synchronized (lipstick) { //先将口红锁上 System.out.println(this.girl+"涂口红。"); //2秒后,照镜子 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (mirror) { //然后将镜子锁上 System.out.println(this.girl+"照镜子。"); } } } } }
执行后,两个线程都在等对方的资源,都处于停滞状态。
死锁的解决方法
死锁是由于“同步块需要同时持有多个对象锁”造成的。要解决这个问题,就是同一个代码块不要同时持有两个对象锁。如上面的死锁例子,可以修改如下:
public class DeadLock { public static void main(String[] args) { new Thread(new MarkUp("大丫", true)).start(); new Thread(new MarkUp("二丫", false)).start(); } } //镜子 class Mirror{ } //口红 class Lipstick{ } //化妆 class MarkUp implements Runnable{ //不管几个对象只有一份 static Mirror mirror=new Mirror(); static Lipstick lipstick=new Lipstick(); private String girl; private boolean flag; public MarkUp(String girl, boolean flag) { this.girl = girl; this.flag = flag; } @Override public void run() { markup(); } //相互持有对方的对象锁 private void markup() { if (flag) { synchronized (mirror) { //先将镜子锁上 System.out.println(this.girl+"照镜子。"); //1秒后,涂口红 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } /* synchronized (lipstick) { //然后将口红锁上 System.out.println(this.girl+"涂口红。"); }*/ } synchronized (lipstick) { //然后将口红锁上 System.out.println(this.girl+"涂口红。"); } }else { synchronized (lipstick) { //先将口红锁上 System.out.println(this.girl+"涂口红。"); //2秒后,照镜子 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } /* synchronized (mirror) { //然后将镜子锁上 System.out.println(this.girl+"照镜子。"); }*/ } synchronized (mirror) { //然后将镜子锁上 System.out.println(this.girl+"照镜子。"); } } } }
题外内容(与线程同步有相关性)
以下内容与线程同步有相关性,仅写了几个例子来描述。
CAS:比较并交换
public class CAS { //库存 private static AtomicInteger stock=new AtomicInteger(5); public static void main(String[] args) { for (int i = 0; i < 6; i++) { new Thread(new Customer()).start(); } } public static class Customer implements Runnable{ @Override public void run() { synchronized (stock) { //模拟延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } Integer left=stock.get(); if (left<1) { System.out.println(Thread.currentThread().getName()+"没抢到,没有库存了"); return; } System.out.println(Thread.currentThread().getName()+"抢到了,第"+left+"件商品,剩余"+left+"件商品。"); stock.set(left-1); } } } }
指令重排
public class HappenBefore { private static int a=0; //变量1 private static boolean flag=false; //变量2 public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 100; i++) { a=0; flag=false; //线程1 读取数据 Thread t1=new Thread(()->{ a=1; flag=true; }); //线程2 更改数据 Thread t2=new Thread(()->{ if (flag) { a*=1; } //指令重排 if (a==0) { System.out.println("Happen before,a->"+a); } }); t1.start(); t2.start(); t1.join(); t2.join(); } } }
可重入锁:锁可以延续使用
public class LockTest { public void test() { //第一次获得锁 synchronized (this) { while (true) { //第二次获得同样的锁 synchronized (this) { System.out.println("ReentrantLock"); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { new LockTest().test(); } }
不可重入锁:锁不可以延续使用
public class LockTest01 { Lock lock=new Lock(); public void a() { lock.lock(); doSomething(); lock.unLock(); } //不可重入 public void doSomething() { lock.lock(); //............ lock.unLock(); } public static void main(String[] args) { new LockTest01().a(); new LockTest01().doSomething(); } } class Lock{ //是否占用 private boolean isLocked=false; //使用锁 public synchronized void lock() { while (isLocked) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } isLocked=true; } //释放锁 public synchronized void unLock() { isLocked=false; notify(); } }
volatile关键字
volatile用于保证数据的同步,也就是可见性(不保证原子性),可以参考以下例子:
public class ValatileTest { private volatile static int num=0; public static void main(String[] args) { new Thread(()->{ while (num==0) { //此处不要编写代码 } }).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } num=1; } }
线程并发协作(生产者-消费者模式)
生产者-消费者模式的基本概念
多线程环境下,经常需要多个线程能够并发和协作。这是,就需要了解一个重要的多线程并发协作模型“生产者-消费者模式”;
- 什么是生产者。生产者指的是负责生产数据的模块(这里的模块指的可能是方法、对象、线程、进程等)。
- 什么是消费者。消费者指的是负责处理数据的模块(这里的模块指的可能是方法、对象、线程、进程等)。
- 什么是缓冲区。消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好数据放入“缓冲区”,消费者从“缓冲区”拿出要处理的数据。
缓冲区是实现并发操作的核心。缓冲区设置有如下3个好处:
- 实现线程的并发协作:有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿出数据处理即可,不需要考虑生产者生产的情况。这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
- 解耦了生产者和消费者。生产者不需要和消费者直接打交道。
- 解决忙闲不均,提高效率。生产者生产数据慢时,但在缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据。
而生产者-消费者模式主要有两种实现方法:管程法以及信号灯法。
线程并发协作(线程通信)的使用情景
- 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。
- 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。
- 在生产者-消费者问题中,仅适用synchronized是不够的。synchronized可以阻止并发更新同一个共享资源,虽然实现了同步,但它不能用来实现不同线程之间的消息传递(通信),这就需要用到线程通信的方法了。
线程通信的常用方法
方法名 | 作用 |
---|---|
final void wait() | 表示线程一直等待,直到得到其他线程通知 |
void wait(long timeout) | 线程等待指定毫秒参数的时间 |
final void wait(long timeout,int nanos) | 线程等待指定毫秒、微秒的时间 |
final void notify() | 唤醒一个处于等待状态的线程 |
final void notifyAll() | 换新同一个对象上所有调用wait()方法的线程,优先级别高的线程优先运行 |
- 注意事项: 以上方法均是java.lang.Object类的方法,只能在同步方法或者同步块中使用,否则会抛出异常。
在实际开发中,尤其是“架构设计”中会大量使用“生产者-消费者”模式。初学者仅需了解作用即可,如果想深入理解架构这一部分内容是相当重要的。
生产者消费者实现方法
以下是生产者-消费者模式的实现方法的实例,可结合概念以及注释理解。
管程法
public class CoTest01 { public static void main(String[] args) { SynContainer container=new SynContainer(); new Thread(new Producer(container)).start(); new Thread(new Consumer(container)).start(); } } //生产者 class Producer implements Runnable{ private SynContainer container; public Producer(SynContainer container) { this.container = container; } @Override public void run() { //生产 for (int i = 0; i < 100; i++) { System.out.println("生产第"+(i+1)+"个面包"); container.push(new Bread(i)); } } } //消费者 class Consumer implements Runnable{ private SynContainer container; public Consumer(SynContainer container) { this.container = container; } @Override public void run() { //消费 for (int i = 0; i < 100; i++) { System.out.println("买了"+(container.get().getId()+1)+"个面包"); } } } //缓冲区 class SynContainer{ Bread[] breads=new Bread[10]; private int count =0; //存储 生产 public synchronized void push(Bread bread) { //缓冲区(库存)满了停止消费 if (count==breads.length) { try { this.wait(); //线程阻塞 停止生产,消费者通知生产解除阻塞 } catch (InterruptedException e) { e.printStackTrace(); } } //容器未满可以生产 breads[count]=bread; count++; //this.notify(); this.notifyAll(); //生产了商品可以通知生产者恢复消费了 } //获取 消费 public synchronized Bread get() { //缓冲区为空(没有面包)就需要停止消费 if (count==0) { try { this.wait(); //线程阻塞 停止消费,生产者通知消费解除阻塞 } catch (InterruptedException e) { e.printStackTrace(); } } //没有数据只能等待 count--; //this.notify(); this.notifyAll(); //消费了商品可以通知生产者恢复生产了 return breads[count]; } } //面包 class Bread{ private int id; public int getId() { return id; } public Bread(int i) { super(); this.id = i; } }
信号灯法
public class CoTest02 { public static void main(String[] args) { Tv tv=new Tv(); new Thread(new Actor(tv)).start(); new Thread(new Audience(tv)).start(); } } //生产者 演员 class Actor implements Runnable{ private Tv tv; public Actor(Tv tv) { this.tv = tv; } @Override public void run() { for (int i = 0; i < 20; i++) { if (i%2==0) { this.tv.play("牛逼"); } else { this.tv.play("666"); } } } } //消费者 观众 class Audience implements Runnable{ private Tv tv; public Audience(Tv tv) { this.tv = tv; } @Override public void run() { for (int i = 0; i < 20; i++) { this.tv.watch(); } } } //同一个资源 电视 class Tv{ private String voice; //信号灯:true表示演员表演,观众等待;false表示观众等待,演员表演 private boolean flag=true; public synchronized void play(String voice){ //演员等待 if (!flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("演员说了:"+voice); this.voice=voice; this.notifyAll(); //唤醒 this.flag=!this.flag; //切换标志 } public synchronized void watch() { //观众等待 if (flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("观众听到了:"+this.voice); this.notifyAll(); //唤醒 this.flag=!this.flag; //切换标志 } }
任务定时调度
任务定时调度在项目开发中经常用到。在实际开发中可以使用quanz任务框架来开发,也可以使用Timer和Timertask类实现同样的功能。
通过Timer和TimerTask类可以实现定时启动某个线程,通过线程执行某个任务的功能。
Timer和Timertask类
java.util.Timer
在这种方式中,Timer类的作用类似于闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer是JDK中提供的一个定时器工具。使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次,起到类似闹钟的作用。
java.util.TimerTask
TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程能力。在这种实现方式中,通过继承TimerTask使用该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。
可以参考以下例子理解:
public class TimerTest { public static void main(String[] args) { Timer timer=new Timer(); //执行安排 //timer.schedule(new MyTimer(), 3000); //3000毫秒后执行1次 //timer.schedule(new MyTimer(), 3000,1000); //3000毫秒后执行,然后每隔1000毫秒执行一次 Calendar calendar=new GregorianCalendar(2020,05,06,20,45,00); //传入一个时间(注意月份0-11) //timer.schedule(new MyTimer(), calendar.getTime()); //按预定的时间执行一次 timer.schedule(new MyTimer(), calendar.getTime(), 1000); //按预定的时间执行,然后每隔1000毫秒执行一次 } } //任务类 class MyTimer extends TimerTask{ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("放空大脑。。。"); } System.out.println("----------END-------------"); } }
在实际使用中,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间需要完全独立的话,最好还是一个Timer启动一个TimerTask。
Quartz的简单例子
使用Quartz框架我们可以到Quartz官网下载开源文件,本文仅描述一个简单的例子,如果想深入了解可以查看文件中的API文档以及源码。
首先我们需要一个创建一个任务的对象:
import java.util.Date; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; public class HelloJob implements Job { public HelloJob() { } public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("------start-------"); System.out.println("Hello World! - " + new Date()); System.out.println("------end-------"); } }
以下是一个简单使用例子:
import static org.quartz.DateBuilder.evenSecondDateAfterNow; import static org.quartz.JobBuilder.newJob; import static org.quartz.TriggerBuilder.newTrigger; import static org.quartz.SimpleScheduleBuilder.simpleSchedule; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; import java.util.Date; /** * Quartz学习入门 * @author WHZ * */ public class SimpleExample { public void run() throws Exception { //1、创建Scheduler工厂 SchedulerFactory sf = new StdSchedulerFactory(); //2、从工厂中获取调度器 Scheduler sched = sf.getScheduler(); //3、创建JobDetail(任务) JobDetail job = newJob(HelloJob.class).withIdentity("job1", "group1").build(); //时间 //Date runTime = evenMinuteDate(new Date()); //下一分钟 Date runTime = evenSecondDateAfterNow(); //下一秒 //4、触发器(触发条件) //Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build(); Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime). //按设定的时间开始运行 withSchedule(simpleSchedule().withIntervalInSeconds(5).withRepeatCount(3)).build(); //间隔5秒,重复3次 //5、注册任务和触发条件 sched.scheduleJob(job, trigger); //6、启动 sched.start(); try { //5秒后停止(该线程总共运行的时间) Thread.sleep(30L * 1000L); } catch (Exception e) { } //7、停止 sched.shutdown(true); } public static void main(String[] args) throws Exception { SimpleExample example = new SimpleExample(); example.run(); } }
实际开发中,可以使用该开源框架更加方便实现任务的定时调度,实际上该框架底层原理就是Timer和TimerTask类的内容,想要深入了解可以尝试阅读QUARTZ框架的源码。
结语
本篇到此完结,多线程的内容在Java中是极其深奥的一部分。碍于本人水平有限,本文中没有描述JUC包的内容,可以参考相关的API文档以及书籍来学习。而对于更加复杂的系统级程序设计,建议参考更高级的参考文献。希望看到这里的读者能点个赞给个关注,祝各位早日年薪百万!