多线程是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文档以及书籍来学习。而对于更加复杂的系统级程序设计,建议参考更高级的参考文献。希望看到这里的读者能点个赞给个关注,祝各位早日年薪百万!