Java线程基础知识
线程
多线程是同时有多个线程并发执行,同时完成多个任务,具有多个顺序执行流,且执行流之间互不干扰
Java语言对多线程有非常优秀的支持
线程和进程
在操作系统中,每一个独立的应用程序都是一个进程,当一个程序进入内存以后就会变成一个进程,
进程是操作系统进行资源分配和调度的独立单位。
进程的三个特征
- 独立性
- 进程用有自己独立的资源,独立的地址空间,其他进程不可以直接访问该进程的地址空间,除非该进程允许
- 动态性
- 程序只是一个静态的指令集合,只有当程序进入内存运行时,才会变成一个正在运行的动态的指令集和,进程具有自己的生命周期和各种不同的状态
- 并发性
- 多个进程可以在单个处理器上并发执行
并发编程的三个概念
- 原子性
一个或多个操作要么全部执行完毕且执行过程不会被中断,要么就不执行
- 可见性
当多个线程同时访问同一个资源时,一个线程如果修改了这个资源,那么其他的线程能够立即看到修改后的值
- 有序性
程序执行的顺序按照代码编写的先后顺序
有序性之指令的重排序:
只要程序执行的最终结果与之顺序执行的结果相同,那么指令的执行顺序就可以不和指令的编写顺序不同。这个改变代码执行顺序的过程就叫做指令的重排序。
JVM可以根据处理器的特性(多核、多级缓存),适当的对指令的执行顺序进行重排序,使得机器的指令可以更贴合CPU的特征,从而可以最大限度的发挥机器的性能
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before(先行发生原则) 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
这就要再把有序性的概念扩展一下了。Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。
所以JVM只能保证单线程中的有序性,如果在多线程中没有针对有序化的限制,就会出现问题。
并发与并行
- 并发
- 并发是在一块cpu上同时运行多个进程,cpu要在多个进程之间进行轮换。并发不是阵正的同时发生,而是在很快的速度进行轮换,由于速度很快,所以感知不强。
- 并行
- 并行是指多个事件在同一时刻同时进行,如今的多核处理器就支持并行处理,如果一个程序对并行进行优化的话,会极大的提升效率
线程是进程的而组成部分,线程是最小的处理单位,可以拥有自己的堆栈,计数器和局部变量,但不能拥有系统资源,多个线程之间共享其所在进程的系统资源。
多线程拓展了多进程的概念,使得同一个进程可以同时并处理多个任务。因此,线程也被称作轻量级进程。多线程和多进程是多任务的两种类型,主要区别如下:
- 多进程之间的数据块是独立的,彼此互不影响,进程之间需要通过信号、管道等进行交互;
- 多线程之间的数据块可以共享,一个进程中的各个线程可以共享程序段、数据段等资源。多线程比多进程更便于资源共享,同时Java提供的同步机制还可以解决线程之间数据完整性的问题
Java线程模型
- 线程分为用户线程和内核线程;
- 线程模型有多对一模型、一对一模型、多对多模型;
- 操作系统一般只实现到一对一模型;
- Java使用的是一对一线程模型,所以它的一个线程对应于一个内核线程,调度完全交给操作系统来处理;
- Go语言使用的是多对多线程模型,这也是其高并发的原因,它的线程模型与Java中的ForkJoinPool非常类似;
- python的gevent使用的是多对一线程模型;
一对一线程模型
- 优点:
- 实现简单
- 缺点:
- 对用户线程的大部分操作都会映射到内核线程上,引起用户态和内核态的频繁切换;
- 内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响;
Java使用的就是一对一线程模型,所以在Java中启一个线程要谨慎。
Java线程模型提供了线程所需的功能支持,,基本的Java线程模型有Thread类,Runnable接口,Callable接口,Future接口等,这些线程模型都是面向对象的。
Thread类的常用方法
构造方法
Thread()
Thread(Runnable target) //创建一个新线程
Thread(Runnable target, String name) //创建一个新线程并命名
Thread(String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
Thread(ThreadGroup group, String name)
前四个构造方法最常用,后四个构造方法则会涉及到线程组。
总之,可以根据需要来选择适当的构造函数,从而指定:线程名字、Runnable对象、所属的线程组和栈大小。
Thread类的静态方法 (类方法)
activeCount(): 返回当前线执行的程所在的线程组中的活动线程的数目
currentThread(): 返回当前正在执行的线程
holdsLock(Object obj): 返回当前执行的线程是否持有指定对象的锁
interrupted(): 返回当前执行的线程是否已经被中断
sleep(long millis): 使当前执行的线程睡眠多少毫秒数
sleep(long millis, int nanos): 使当前执行的线程睡眠多少毫秒数+纳秒数
yield(): 使当前执行的线程自愿暂时放弃对处理器的使用权并允许其他线程执行
Thread类的非静态方法 (实例方法)
getId(): 返回该线程的id
getName(): 返回该线程的名字
getPriority(): 返回该线程的优先级
getState(): 返回该线程状态
interrupt(): 使该线程中断
isInterrupted(): 返回该线程是否被中断
isAlive(): 返回该线程是否处于活动状态
isDaemon(): 返回该线程是否是守护线程
join(): 等待该线程终止
join(long millis): 等待该线程终止,至多等待多少毫秒数
join(long millis, int nanos): 等待该线程终止,至多等待多少毫秒数+纳秒数
start(): 使该线程开始执行
toString(): 返回该线程的信息——名字、优先级和所属线程组
setDaemon(boolean on): 将该线程标记为守护线程或用户线程,如果不标记默认是非守护线程
setName(String name): 设置该线程的名字
setPriority(int newPriority): 改变该线程的优先级
线程类的创建
Java里创建线程常见的且比较简单方式主要有下面三种:
继承Thread类,并且重写run方法
实例:
TestThread.java
package com.company;
public class TestThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//继承Thread类时,直接使用this即可获取当前线程对象
System.out.println(this.getName() + ":" + i);
}
}
}
Main.java
package com.company;
public class Main {
static public void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread() + ":" + i);
}
}
}
很显然,由于java不支持对类的多继承,所以如果继承了Thread类就没办法再继承其它父类了,所以更建议使用如下的第二种方式。
实现Runnab接口
Runnable接口中只有一个run()方法,一个类实现了Runnable接口方法后并不算是一个线程类,不能直接启动线程,必须通过Thread类的实例来创建并启动线程;
实例:
TestRunnable.java
package com.company;
public class TestRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//实现Runnable接口后就不能直接使用this获取当前线程对象了
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
RunnableMain.java
package com.company;
public class RunnableMain {
public static void main(String[] args) {
Thread thread = new Thread(new TestRunnable(),"线程1");
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread() + ":" + i);
}
}
}
使用Callable和Future接口
使用使用Callable和Future接口的最大优势在于可以在线程执行完任务以后获取执行结果
使用使用Callable和Future接口创建并启动线程的步骤如下:
- 创建Callable接口的实现类,并实现call()方法,将该方法作为程序的执行体,并具有返回值
- 使用FutureTask类来包装Callable对象,Callable对象的返回值也会被封装在FutureTask对象里
- 使用FutureTask对象作为Thread的target,创建并启动线程
- 可以调用FutureTask对象的get()方法来获得子线程的返回值
实例
package com.company;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//创建Callable接口的实现类,并实现call()方法
class Task implements Callable <Integer>{
@Override
public Integer call() throws Exception {
int i = 0;
for (;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
return i;
}
}
public class CallableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//使用FutureTask类来包装Callable对象
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Task());
//使用FutureTask对象作为Thread的target,创建并启动线程
Thread thread = new Thread(futureTask);
thread.start();
//可以调用FutureTask对象的get()方法来获得子线程的返回值
System.out.println("futureTask.get() = " + futureTask.get());
}
}
也可以使用Lambda表达式创建Callable对象
package com.company;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class LambdaCallableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(
(Callable<Integer>)() -> {
int i = 0;
for (;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
return i;
}
);
//使用FutureTask对象作为Thread的target,创建并启动线程
Thread thread = new Thread(futureTask);
thread.start();
//可以调用FutureTask对象的get()方法来获得子线程的返回值
System.out.println("futureTask.get() = " + futureTask.get());
}
}
线程的生命周期
只能对新建(new)状态的线程使用start()方法,否则会引发IllegalThreadStateExpression异常
运行和阻塞状态
线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于操作系统采取的策略。UNIX系统采用的是时间片轮转算法,windows采用的是抢占式算法,也有一些小型设备采用协作式调度算法(这样的算法需要程序主动放弃所占用的资源);
出现一下情况时,线程会进入阻塞状态:
- 线程调用了sleep()方法,主动放弃所占用的资源
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程会被阻塞
- 线程视图获得一个同步监视器,但是该监视器正在被其他线程持有
- 执行条件未满足,调用wait方法使线程进入等待状态,等待其他线程的通知
- 程序调用了suspend()方法将该线程挂起,该方法容易造成死锁,尽量避免使用
当以上条件失效以后,会从阻塞态转换为就绪态
死亡状态
- 执行完成,正常结束
- 抛出异常或发生错误
- 调用stop()方法直接停止线程,该方法容易造成死锁,尽量避免使用
子线程一旦启动起来,就会拥有和主线程一样的地位。所以主线程结束时,其他子线程并不会受影响。
可以通过线程对象的isAlive()方法来获得线程的状态(false:死亡),不要视图对一个已经死亡的线程调用start()方法,人死不能复生,节哀顺变
线程优先级
每个线程执行时都会具有一定的优先级,当多个线程处于就绪状态时,优先级高的线程将有更大的几率获得CUP资源
每个线程都会有默认的优先级,其优先级都与创建该线程的父类线程的优先级相同。在默认的情况下为默认优先级
Thread提供了三个静态常量来标识线程的优先级
- MAX_PRIORITY 最高优先级,其值为10
- NORM_PRIORITY 普通优先级,其值为5,该值为默认优先级
- MIN__PRIORITY 最低优先级,其值为1
Thread类提供了setPriority()方法来设置线程优先级,getPriority()方法来获取线程的优先级。
setPriority()方法的参数可以是一个整数,也可以使用Thread类提供的三个优先级静态常量
线程的优先级依赖于操作系统,并不是所有的操作系统都支持Java的是个优先级级别,因此,尽量避免直接使用整数给线程指定优先级
优先级并不能保证线程执行的顺序,只是分配资源的概率大小,因此应该避免使用优先级作为构建任务执行顺序的标准;
控制线程执行顺序的几种方法
通常情况下,线程的执行顺序都是随机的,哪个获取到CPU的时间片,哪个就获得执行的机会。不过实际的项目中有时我们会有需要不同的线程顺序执行的需求。借助一些java中的线程阻塞和同步机制,我们往往也可以控制多个线程的执行顺序。
利用Thread类的join()方法实现线程的顺序执行
join()方法可以让一个线程等待另一个线程执行完以后,继续执行原线程中的任务。当某个线程在执行过程中调用了其他线程的join()方法,当前线程就会被阻塞,知道调用的线程执行结束。join()方法通常由使用线程的程序调用,等待其他线程执行结束后,再进行下一步操作
实例:
JoinThread.java
package com.company;
public class JoinThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName() + ":" + i);
}
}
}
JoinDemo.java
package com.company;
public class JoinDemo {
public static void main(String[] args) {
JoinThread joinThread = new JoinThread();
joinThread.start();
try {
joinThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
运行结果
Thread-0:0
Thread-0:1
....
Thread-0:8
Thread-0:9
main:0
main:1
main:2
....
main:8
main:9
利用 CountDownLatch 控制线程的执行顺序
COuntDownLatch是一个同步工具类,位于java.util.concurrent包下,利用它可以使线程实现类似计数器的功能
CountDownLatch类只提供了一个构造器:
public CountDownLatch(int count) { }; //参数count为计数值
CountDownLatch提供两个核心的方法,countDown和await,当线程调用await方法时,会阻塞当前线程,直到count归零才会继续执行。而countDown方法则可以使count减一
借鉴的图:
实例1:
主线程会等有若干个子线程执行完毕之后再执行,不过这若干个子线程之间的执行顺序是随机的。
package com.company;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ControlThreadDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
List<Thread> workers = Stream.generate(() -> new Thread(new CountDownDemo(countDownLatch))).limit(5).collect(Collectors.toList());
workers.forEach(Thread::start);
countDownLatch.await();
System.out.println("主线程执行完毕");
}
}
class CountDownDemo implements Runnable{
private CountDownLatch countDownLatch;
public CountDownDemo(CountDownLatch latch) {
this.countDownLatch = latch;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
countDownLatch.countDown();
}
}
结果如下:
线程Thread-0开始执行
线程Thread-3开始执行
线程Thread-4开始执行
线程Thread-2开始执行
线程Thread-1开始执行
主线程执行完毕
实例2:
利用 CountDownLatch(倒计时门闩shuan) 控制一组线程一起执行。就好像在运动场上,教练的发令枪一响,所有运动员一起跑。我们一般在模拟线程并发执行的时候会用到这种场景。
程序逻辑:
- 创建一个等待发令计数门闩count为5,一个发令门闩count为1,一个结束门闩count为5
- 创建五名运动员,并且开始入场,每当一个运动员入场完毕,等待发令计数门闩就减1,同时调用发令门闩的await()方法开始等待发令
- 当五名运动员入场完毕后,裁判员开始发令,使发令门闩的count减1至0,此时,五名运动员开始运行,运行结束后,结束门闩的count减1
- 结束门闩的count减至0以后,裁判员宣布比赛结束
package com.company;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ControlThreadDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch readyLatch = new CountDownLatch(5);
CountDownLatch runningLatchWait = new CountDownLatch(1);
CountDownLatch completeLatch = new CountDownLatch(5);
List<Thread> workers = Stream.generate(() -> new Thread(new CountDownDemo2(readyLatch,runningLatchWait,completeLatch))).limit(5).collect(Collectors.toList());
System.out.println("运动员入场");
workers.forEach(Thread::start);
readyLatch.await();//等待发令
System.out.println("\n准备开始");
runningLatchWait.countDown();//发令
completeLatch.await();//等所有子线程执行完
System.out.println("\n比赛结束");
}
}
class CountDownDemo2 implements Runnable{
private CountDownLatch readyLatch;
private CountDownLatch runningLatchWait;
private CountDownLatch completeLatch;
public CountDownDemo2(CountDownLatch readyLatch, CountDownLatch runningLatchWait, CountDownLatch completeLatch) {
this.readyLatch = readyLatch;
this.runningLatchWait = runningLatchWait;
this.completeLatch = completeLatch;
}
@Override
public void run() {
readyLatch.countDown();
System.out.println("线程"+Thread.currentThread().getName()+"准备好了");
try {
runningLatchWait.await();
System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("线程"+Thread.currentThread().getName()+"运行结束");
completeLatch.countDown();
}
}
}
运行结果:
运动员入场
线程Thread-1准备好了
线程Thread-0准备好了
线程Thread-2准备好了
线程Thread-3准备好了
线程Thread-4准备好了
准备开始
线程Thread-1开始执行
线程Thread-2开始执行
线程Thread-2运行结束
线程Thread-0开始执行
线程Thread-0运行结束
线程Thread-1运行结束
线程Thread-4开始执行
线程Thread-4运行结束
线程Thread-3开始执行
线程Thread-3运行结束
比赛结束
利用单线程化线程池 [线程池单开一章记录吧]
JAVA通过Executors提供了四种线程池,单线程化线程池(newSingleThreadExecutor)、可控最大并发数线程池(newFixedThreadPool)、可回收缓存线程池(newCachedThreadPool)、支持定时与周期性任务的线程池(newScheduledThreadPool)。
顾名思义,newSingleThreadExecutor 线程池只有一个线程。它存在的意义就在于控制线程执行的顺序,保证任务的执行顺序和提交顺序一致。其实保证顺序执行的原理也很简单,因为总是只有一个线程处理任务队列上的任务,先提交的任务必将被先处理。
实例:
public static void main(String[] args) throws InterruptedException {
final ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++){
final int index = i;
executorService.execute(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName("thread-" + index);
System.out.println("线程: " + Thread.currentThread().getName() + " 开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
executorService.awaitTermination(10, TimeUnit.SECONDS);
executorService.shutdownNow();
}
运行结果:
线程: thread-0 开始执行
线程: thread-1 开始执行
线程: thread-2 开始执行
线程: thread-3 开始执行
线程: thread-4 开始执行
线程: thread-5 开始执行
线程: thread-6 开始执行
线程: thread-7 开始执行
线程: thread-8 开始执行
线程: thread-9 开始执行
线程同步
多线程访问统一资源时,很容易出现线程安全问题。
试想一下,当多个人同时去多个取款机取同一张卡里的钱时,1号取款机获取到余额有100元,于是1号机就可以取出100元,但是在取款机查询完余额并且还没有取款的一瞬间,操作系统将处理器资源分配给了2号机,由于1号机的取款操作未完成,2号机也查询到了账户余额有100元,并且顺利取了出来。这样一来就会出现损失。
因此,在Java中提供了线程同步的概念以保证某个资源在某一时刻只能由一个线程访问。
Java使用监视器(对象锁)实现同步。每个对象都会有一个监视器,使用监视器可以保证每次只能由一个线程访问该对象的同步语句。在该对象的同步语句执行结束之前,所有想要执行该对象的同步语句的线程都将处于阻塞态,直到当前对象的同步语句执行完毕后,监视器才会释放对象锁,并让优先级最高的阻塞线程执行同步语句。
实例:
BankHall.java
package com.company;
public class BankHall {
public static void main(String[] args) {
//模拟多个柜台同时操作同一个账户
//新建一个账户
BankAccount bankAccount = new BankAccount("10086",5000d);
//创建多个柜台机线程,对账户进行取钱操作
Thread thread1 = new Thread(new NoSynBank(bankAccount,-3000), "柜台1");
Thread thread2 = new Thread(new NoSynBank(bankAccount,-3000), "柜台2");
Thread thread3 = new Thread(new NoSynBank(bankAccount,1000), "柜台3");
Thread thread4 = new Thread(new NoSynBank(bankAccount,-2000), "柜台4");
Thread thread5 = new Thread(new NoSynBank(bankAccount,2000), "柜台5");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
//主线程等待五台柜台机执行完毕
try {
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("bankAccount.toString() = " + bankAccount.toString());
}
}
NoSynBank.java
package com.company;
/**
* 银行类
*/
public class NoSynBank implements Runnable{
private BankAccount bankAccount;
private double money;
public NoSynBank(BankAccount bankAccount, double money) {
this.bankAccount = bankAccount;
this.money = money;
}
@Override
public void run() {
synchronized (this.bankAccount){
//获取目前账户的余额
double balance = this.bankAccount.getBalance();
//判断是否要取钱,如果是,判断余额是否足够
if (money < 0 && (money*-1) <= balance){
bankAccount.setBalance(balance+money);
System.out.println(Thread.currentThread().getName()+"取钱成功,余额剩余:"+bankAccount.getBalance());
}else if (money < 0 && (money*-1) > balance){
System.out.println(Thread.currentThread().getName()+"余额不足");
}else {
bankAccount.setBalance(balance+money);
System.out.println(Thread.currentThread().getName()+"存钱成功,余额剩余:"+bankAccount.getBalance());
}
}
}
}
BankAccount.java
package com.company;
/**
* 银行账户类
*/
public class BankAccount {
private String bankNo;
@Override
public String toString() {
return "BankAccount{" +
"bankNo='" + bankNo + '\'' +
", balance=" + balance +
'}';
}
private double balance;
public BankAccount(String bankNo, Double balance) {
this.bankNo = bankNo;
this.balance = balance;
}
public String getBankNo() {
return bankNo;
}
public void setBankNo(String bankNo) {
this.bankNo = bankNo;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
结果:
柜台1余额不足
柜台3存钱成功,余额剩余:3000.0
柜台5存钱成功,余额剩余:3000.0
柜台4取钱成功,余额剩余:1000.0
柜台2取钱成功,余额剩余:3000.0
bankAccount.toString() = BankAccount{bankNo='10086', balance=3000.0}
同步代码块
使用同步代码块可以实现同步功能,只需将需要同步的语句放入一个同步代码块中
语法:
synchronized(object){
//需要同步的代码块
}
- sychronized:关键字
- object:同步监视器,在线程处理同步代码之前需要先获取对同步监视器的锁定,如果已经处于锁定状态,线程就会开始阻塞
使用同步代码块对银行账户进行锁定:
NoSynBank.java
package com.company;
/**
* 银行类
*/
public class NoSynBank implements Runnable{
private BankAccount bankAccount;
private double money;
public NoSynBank(BankAccount bankAccount, double money) {
this.bankAccount = bankAccount;
this.money = money;
}
@Override
public void run() {
synchronized (this.bankAccount){
//获取目前账户的余额
double balance = this.bankAccount.getBalance();
//判断是否要取钱,如果是,判断余额是否足够
if (money < 0 && (money*-1) <= balance){
bankAccount.setBalance(balance+money);
System.out.println(Thread.currentThread().getName()+"取钱成功,余额剩余:"+bankAccount.getBalance());
}else if (money < 0 && (money*-1) > balance){
System.out.println(Thread.currentThread().getName()+"余额不足");
}else {
bankAccount.setBalance(balance+money);
System.out.println(Thread.currentThread().getName()+"存钱成功,余额剩余:"+bankAccount.getBalance());
}
}
}
}
修改后的运行结果:
柜台1取钱成功,余额剩余:2000.0
柜台5存钱成功,余额剩余:4000.0
柜台4取钱成功,余额剩余:2000.0
柜台3存钱成功,余额剩余:3000.0
柜台2取钱成功,余额剩余:0.0
bankAccount.toString() = BankAccount{bankNo='10086', balance=0.0}
同步方法
同步方法是使用synchronized关键字修饰的的需要同步的方法,语法格式:
[访问修饰符] synchronized 返回类型 方法名([参数列表]){
//方法体
}
- 使用同步方法不需要指定同步监视器,同步方法的同步监视器就是该方法所属的对象
- 一旦一个线程进入了该同步方法,那么其他要访问该同步方法的线程都会进入阻塞态,但是该类的其他方法不受影响
将上述代码中的银行账户类中增加一个同步方法用来操作余额,实例如下:
BankAccount.java
package com.company;
/**
* 银行账户类
*/
public class BankAccount {
private String bankNo;
private double balance;
@Override
public String toString() {
return "BankAccount{" +
"bankNo='" + bankNo + '\'' +
", balance=" + balance +
'}';
}
public BankAccount(String bankNo, Double balance) {
this.bankNo = bankNo;
this.balance = balance;
}
public String getBankNo() {
return bankNo;
}
public void setBankNo(String bankNo) {
this.bankNo = bankNo;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
public synchronized void access(double money){
//获取目前账户的余额
double balance = this.getBalance();
//判断是否要取钱,如果是,判断余额是否足够
if (money < 0 && (money*-1) <= balance){
this.setBalance(balance+money);
System.out.println(Thread.currentThread().getName()+"取钱成功,余额剩余:"+this.getBalance());
}else if (money < 0 && (money*-1) > balance){
System.out.println(Thread.currentThread().getName()+"余额不足");
}else {
this.setBalance(balance+money);
System.out.println(Thread.currentThread().getName()+"存钱成功,余额剩余:"+this.getBalance());
}
}
}
NoSynBakn.java
package com.company;
/**
* 银行类
*/
public class NoSynBank implements Runnable{
private BankAccount bankAccount;
private double money;
public NoSynBank(BankAccount bankAccount, double money) {
this.bankAccount = bankAccount;
this.money = money;
}
@Override
public void run() {
bankAccount.access(money);
}
}
运行结果:
柜台3存钱成功,余额剩余:6000.0
柜台5存钱成功,余额剩余:8000.0
柜台2取钱成功,余额剩余:5000.0
柜台1取钱成功,余额剩余:2000.0
柜台4取钱成功,余额剩余:0.0
bankAccount.toString() = BankAccount{bankNo='10086', balance=0.0}
同步锁 【这里简单介绍一下,单开一章详细讲】
同步锁是一种更强大的线程同步机制,通过显式定义同步锁对象来实现线程同步
相比较于同步方法与同步代码块,同步锁更加灵活,操作更加广泛
这里介绍一下ReentrantLock(可重入锁)通常的使用步骤:
- 定义ReentrantLock锁对象,该对象是final常量
private final ReedtrantLock lock = new ReedtrantLock();
- 在需要保证线程安全的代码之前进行加锁操作
lock.lock();
- 在执行完线程安全代码之后执行释放锁
lock.unlock()
上面的银行类代码进行加锁改造:
BankAccount.java
package com.company;
import java.util.concurrent.locks.ReentrantLock;
/**
* 银行账户类
*/
public class BankAccount {
private String bankNo;
private double balance;
private final ReentrantLock lock = new ReentrantLock();
@Override
public String toString() {
return "BankAccount{" +
"bankNo='" + bankNo + '\'' +
", balance=" + balance +
'}';
}
public BankAccount(String bankNo, Double balance) {
this.bankNo = bankNo;
this.balance = balance;
}
public String getBankNo() {
return bankNo;
}
public void setBankNo(String bankNo) {
this.bankNo = bankNo;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
public void access(double money){
lock.lock();
try {
//获取目前账户的余额
double balance = this.getBalance();
//判断是否要取钱,如果是,判断余额是否足够
if (money < 0 && (money*-1) <= balance){
this.setBalance(balance+money);
System.out.println(Thread.currentThread().getName()+"取钱成功,余额剩余:"+this.getBalance());
}else if (money < 0 && (money*-1) > balance){
System.out.println(Thread.currentThread().getName()+"取款"+money*-1+"余额不足");
}else {
this.setBalance(balance+money);
System.out.println(Thread.currentThread().getName()+"存钱成功,余额剩余:"+this.getBalance());
}
}finally {
lock.unlock();
}
}
}
运行结果:
柜台1取钱成功,余额剩余:2000.0
柜台2取款3000.0余额不足
柜台3存钱成功,余额剩余:3000.0
柜台4取钱成功,余额剩余:1000.0
柜台5存钱成功,余额剩余:3000.0
bankAccount.toString() = BankAccount{bankNo='10086', balance=3000.0}
线程通信
线程通信的目的就是让线程之间具备发送信号的能力。假设现在系统中有生产和消费两个线程,要求不断重复生产,消费的动作。但是生产完以后就要立即消费,不允许连续生产两次,也不允许连续消费两次。为了实现这种功能,就可以使用线程之间的通信。
Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM(Java内存模型)就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。
其实JMM并不像JVM内存结构一样是真实存在的。他只是一个抽象的概念。JSR-133: Java Memory Model and Thread Specification中描述了,JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。
利用共享对象实现通信
就像是在生产者消费者中间放了一块打着叉号公示牌(没有产品)。生产者生产完毕后会在牌子上打一个对勾(存在产品),打上对勾以后就会一直盯着,在公示牌上的信号变成叉号之后再次生产。与此同时,消费者也一直在盯着公示牌,当公示牌上的信号变成对勾以后就开始消费,消费后将公示牌上的信号改成叉号,往复循环......
忙等(busy waiting)
消费者执行的条件是生产者成功生产出商品,所以消费者一直在等待信号,在一个循环的检测条件中。此时消费者就处于一个忙等的状态,因为他在等待过程中是忙碌的。同时生产者生产完产品以后也会进入忙等的状态。在忙等状态中,该线程依旧消耗资源,所以要尽量避免线程进入忙等状态,除非忙等的时间非常短
wait(), notify() and notifyAll()
- wait 让当前线程等待,并释放对象锁,知道其他线程调用该监视器的notify()方法或者notifyAll()来唤醒此线程。wait()也可以指定一个参数,自定义等待的时间,这样就不需要notify()或者notifyAll()的唤醒
- notify()方法:唤醒在此同步器上等待的单个线程,解除该线程的阻塞状态
- nitifyAll():唤醒在此同步器上等待的所有线程,唤醒次序完全由系统控制