JavaSE——线程详解
程序、进程、线程
- 程序是数据和指令的有序集合,是静态的概念。
- 进程是程序的一次执行,是系统资源分配的基本单位,是动态的概念。
- 一个进程可以包含多个线程,但至少有一个线程,线程是CPU调度和执行的基本单位,线程共享所属进程的所有资源,每个进程拥有少量的资源以维持自己的工作。
线程的创建
方式一(Thread):
因为Java只支持单继承,使用继承的方式实现线程有局限性
public class Test01 {
//继承Thread类,重写run()方法,调用start()方法,实现线程的开启
public static void main(String[] args) {
//创建一个线程对象
TestThread01_1 testThread01_1 = new TestThread01_1();
//调用start()方法
testThread01_1.start();
}
}
//1.继承Thread类
class TestThread01_1 extends Thread{
//重写run()方法
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
方式二(Runnable):
避免了单继承的局限性,方便一个对象供多个线程共享使用
public class Test02 {
//实现Runnable对象,重写run()方法,创建实现类对象,创建线程对象,利用线程对象代理实现类对象调用start()来开启线程
public static void main(String[] args) {
//3.创建Runnable接口的实现类对象
TestRunnable02_01 t1 = new TestRunnable02_01();
//4.创建一个Thread类对象,代理我们的实现类对象,通过Thread类对象,调用start()方法,来帮助我们开启线程,利用到的就是 设计模式的静态代理模式
new Thread(t1).start();
}
}
//1.实现Runnable接口
class TestRunnable02_01 implements Runnable{
//2.重写run()方法
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
方式三(Callable):
线程运行结束后有返回值
public class Test03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//3.创建Callable实现类对象
TestCallable03_01 t1 = new TestCallable03_01();
TestCallable03_01 t2 = new TestCallable03_01();
TestCallable03_01 t3 = new TestCallable03_01();
//4.创建执行服务,将需要开启的线程的数量传进去
ExecutorService executorService = Executors.newFixedThreadPool(3);
//5.提交执行
Future<Boolean> future1 = executorService.submit(t1);
Future<Boolean> future2 = executorService.submit(t2);
Future<Boolean> future3 = executorService.submit(t3);
//获取返回值结果
boolean result1 = future1.get();
boolean result2 = future2.get();
boolean result3 = future3.get();
System.out.println(result1);
System.out.println(result2);
System.out.println(result3);
//4.关闭服务
executorService.shutdown();
}
}
//1.实现Callable接口,需要确定泛型的具体类型
class TestCallable03_01 implements Callable<Boolean> {
//2.重写call()方法
@Override
public Boolean call() throws Exception {
System.out.println(Thread.currentThread().getName());
return true;
}
}
Lambda表达式
为什么要引入Lambda表达式?
public class Test04 {
//实现类2
static class InsideWay implements Lambda{
@Override
public void lambda() {
System.out.println("内部类的方式");
}
}
public static void main(String[] args) {
//方式一:外部类
OutSideWay outSideWay = new OutSideWay();
outSideWay.lambda();
//方式二:静态内部类
Test04.InsideWay insideWay = new InsideWay();
insideWay.lambda();
//方式三:局部内部类
class PartialWay implements Lambda{
@Override
public void lambda() {
System.out.println("局部类的方式");
}
}
PartialWay partialWay = new PartialWay();
partialWay.lambda();
//方式四:匿名内部类,没有类名,必须借助父类或者接口。
Lambda anonymousWay = new Lambda() {
@Override
public void lambda() {
System.out.println("匿名内部类的方式");
}
};
anonymousWay.lambda();
//方式五:Lambda表达式
Lambda lambdaWay = ()->{
System.out.println("lambda表达式的方式");
};
lambdaWay.lambda();
}
}
//如果一个接口只有一个方法则称这个接口为函数式接口
interface Lambda{
void lambda();
}
//实现类1
class OutSideWay implements Lambda{
@Override
public void lambda() {
System.out.println("外部类的方式");
}
}
显然lambda表达式可以减少不必要的代码,只保留核心的逻辑
注意:
//当方法只有一个参数的时候,可以去掉圆括号和数据类型,保留一个变量名
lambda = a ->{System.out.println("I love lambda,it's so easy!")};
//当方法有多个参数的时候,不可以去掉圆括号,数据类型必须同时保留或者同时去掉
lamba = (a , b) - > {System.out.println("I love lambda,it's so easy!")};
lamba = (String a ,String b) - > {System.out.println("I love lambda,it's so easy!")};
//当方法体只有一条语句时,花括号可以去掉,有多条语句则不能。
lamba = (a , b) - > System.out.println("I love lambda,it's so easy!");
lamba = (a , b) - > {
System.out.println("I love lambda,it's so easy!")
System.out.println("oh!I'm just lying to you!")
};
//当方法有返回值的时候,无论有几条语句,都不能省略花括号
lamba = (a , b) - > {return a + b};
lamba = (a , b) - > {
a++;
b--;
return a*b;
};
线程控制
线程的状态:
-
创建状态:new语句刚创建的线程对象处于此状态。
-
就绪状态:调用start()方法后的对象进入就绪状态,等待CPU的调度执行。
-
运行状态:CPU正在调度执行。
-
阻塞状态:线程因为某些原因放弃CPU,进入此状态时,不会再被调度执行,直到所期待的事件发生重新进入就绪状态。
阻塞状态可分为以下3种:
位于对象等待池中的阻塞状态(Blocked in object’s wait pool):
当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,这涉及到“线程 通信”的内容。
位于对象锁池中的阻塞状态(Blocked in object’s lock pool):
当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线 程放到这个对象的锁池中,这涉及到“线程同步”的内容。
其他阻塞状态(Otherwise Blocked):
当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。 -
终止状态:线程执行完run()方法中所有的语句后,进入死亡状态,处于死亡状态的线程不能再次启动。
线程状态的转换:
线程的主要方法:
停止线程
//不推荐JDK提供的stop()或者destroy(),这些方法已被废弃
//推荐使用一个标志位来结束进程
public class Test07 {
public static void main(String[] args) {
TestStop testStop = new TestStop();
Thread thread =new Thread(testStop,"testStop");
thread.start();
//当主线程运行次数超过5000次时,结束testStop这个线程
for (int i = 0; i < 100000; i++) {
if(i == 50000)
testStop.stop();
System.out.println(i);
}
}
}
class TestStop implements Runnable{
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
System.out.println(Thread.currentThread().getName() + i++);
}
}
public void stop(){
flag = false;
}
}
线程休眠
public class Test08 {
public static void main(String[] args) {
TicketSystem ticketSystem = new TicketSystem();
Thread t1 = new Thread(ticketSystem,"小明");
t1.start();
Thread t2 = new Thread(ticketSystem,"小红");
t2.start();
Thread t3 = new Thread(ticketSystem,"黄牛");
t3.start();
}
}
//当系统运行速度过快的时候,线程休眠可以放大程序要表达的效果
class TicketSystem implements Runnable{
//票数
private int ticketNum = 20;
@Override
public void run() {
while (ticketNum > 0){
try {
//sleep方法需要一个时间的参数,单位是毫秒。线程执行sleep()方法后会休眠指定时间,处于阻塞状态,直至到达指定时间,重新进入就绪状态,等待处理机的调度。线程执行sleep方法时不会释放掉当前对象的锁。
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买了第" + ticketNum-- + "张票");
}
}
}
线程礼让
public class Test09 {
//线程即使礼让了,进入就绪状态,也还是会被CPU调度执行,可能成功,也可能失败
public static void main(String[] args) {
Yield yield = new Yield();
Thread t1 = new Thread(yield);
Thread t2 = new Thread(yield);
t1.start();
t2.start();
}
}
class Yield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在运行");
Thread.yield(); //当前线程进行礼让
System.out.println(Thread.currentThread().getName() + "正在运行");
}
}
线程插队
public class Test01 {
public static void main(String[] args) throws InterruptedException {
TestJoin testJoin = new TestJoin();
testJoin.start();
for (int i = 0; i < 1000; i++) {
//当主线程运行500次时让testJoin这个线程插队
if (i == 500) {
//让testJoin插在主线程的前面,直到该线程结束,才能回到主线程
testJoin.join();
}
System.out.println(Thread.currentThread().getName() + i);
}
}
}
class TestJoin extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(this.getName() + "正在运行!" + i);
}
}
}
线程状态观测
在Java中线程总共有如下几种状态,保存在Thread. State中,与上述线程之间的状态转换中的种类有些许差异:
名称 | 解释 |
---|---|
NEW | 尚未启动的线程处于此状态 |
RUNNABLE | 在Java虚拟机中执行的线程处于此状态 |
BLOCKED | 被阻塞等待监听器锁定的线程出于此状态 |
WAITING | 正在等待另一个线程执行特定的动作的线程处于此状态 |
TIMED_WAITING | 正在等待另一个线程执行指定的动作到达某一个特定的时间处于此状态 |
TERMINATED | 已退出的线程处于此状态 |
一个线程可以在给定时间点处于一个状态,这些状态时不反映任何操作系统线程状态的虚拟机状态,可能这也是上述有些许差异的原因吧。
public class Test02 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + i);
}
});
//输出当前状态
System.out.println(thread.getState()); //NEW
//输出启动后的状态
thread.start();
System.out.println(thread.getState()); //RUNNABLE
while (thread.getState() != Thread.State.TERMINATED){
//只要线程不终止就一直监视着该线程的状态
System.out.println(thread.getState());
}
}
}
线程优先级
getPriority() 获得线程的优先级
setPriority() 设置线程的优先级
优先级由1~10,Java的Thread类中有三个优先级常量:
Thread.MIN_PRIORITY = 1
Thread.NORM_PRIORITY = 5
Thread.MAX_PRIORITY = 10
系统默认为5,优先级的设置建议在start()之前
优先级高只是意味着被CPU调度的概率高,优先级低只是意味着被CPU调度的概率低,调不调度,完全看CPU的心情。
守护线程
/*
* 1. 线程分用户线程和守护线程
* 2. 虚拟机必须保证用户线程执行完毕
* 3. 虚拟机不用等待守护线程是否执行完成
* 4. 常见的守护线程有垃圾回收(GC),后台操作记录日志等
*/
public class Test03 {
public static void main(String[] args) {
God god = new God();
Human human = new Human();
Thread thread1 = new Thread(god);
Thread thread2 = new Thread(human,"zhangsan");
//设置god为守护线程
thread1.setDaemon(true); //默认是false,表示是用户线程,true是守护线程
thread1.start();
thread2.start();
}
}
//守护线程
class God implements Runnable {
@Override
public void run() {
while (true){
System.out.println("God will bless you forever!");
}
}
}
//用户线程
class Human implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + i + "岁了");
}
System.out.println("GoodBye World!");
}
}
线程互斥和同步
操作系统中的进程互斥与同步
-
互斥
同一个时间点只允许一个进程访问某个临界资源,并发执行的进程之间因为因为这种原因形成一种间接制约的关系,若不进行控制会产生一些问题。
-
同步
并发执行的进程之间可能需要相互合作,协调工作,它们之间可能必须有先后顺序的关系,并发执行的进程之间因为因为这种原因形成一种直接制约的关系,如不控制也会产生一些问题。
-
临界区:程序中需要访问临界资源的那段代码。
我们可以采用信号量机制实现进程互斥和同步:
//s表示资源数目
//P操作
wait(int s){
s--; //申请资源
if(s <= 0){
block(); //当没有剩余资源可供分配的时候将该进程插入到该资源的阻塞队列中
}
}
//V操作
signal(int s){
s++; //释放资源
if(s < 0){
wakeup(); //当该资源的阻塞队列中还有被阻塞的进程的时候需要将之唤醒
}
}
信号量就是一个低级的进程通信,将某种动作信息通知给另一个进程。
用PV操作实现进程互斥:
在访问临界资源前执行P操作,申请资源,告诉其它需要访问该资源的进程要一直等待,直到它访问结束。
P(s) //s等于1
临界区
V(s)
在访问完临界资源后执行V操作,释放资源,告诉其它正在等待的进程可以访问该资源了
用PV操作实现进程同步:
有三个信号量s1=0、s2=0、s3=0,分别表示S1给S3、S2给S3、S3给S4的信息,0表示还未执行,1表示已执行完成
//S1开始执行前没有限制
S1的代码
V(s1) //S1执行后要告诉S3它已执行完成
--------------------------------------------
//S2开始执行前也没有限制
P(S2)
S2的代码
V(s2) //S2执行后也要告诉S3它已执行完成
---------------------------------------------
P(s1)
p(s2) //S3执行前必须收到S1和S2执行完成的信息
S3的代码
v(s3) //S3执行后要告诉S4它已执行完成
-----------------------------------------------
P(s3) //S4执行S3执行前必须收到S3执行完成的信息
S4的代码
我们可以将上述两种方法结合解决进程互斥和同步综合在一起的问题
Java中的线程互斥和同步
线程互斥
Java中多个线程操作同一个对象的共享数据,会产生一些问题,可以通过锁机制解决,这个解决方案很类似用PV操作解决进程互斥,当一个线程进入一个对象操作共享数据,会获得这个对象的排它锁,其他进程只能进入这个对象的等待池,形成队列,直到获得排它锁才能操作对象的共享数据。
synchronized方法
public class Test05 {
public static void main(String[] args) {
TicketSystem ticketSystem = new TicketSystem();
new Thread(ticketSystem,"小明").start();
new Thread(ticketSystem,"王老师").start();
new Thread(ticketSystem,"黄牛党").start();
}
}
class TicketSystem implements Runnable{
private int num = 10; //票数
private boolean flag = true; //是否有票
private synchronized void buy(){
if (num <= 0) {
System.out.println("票卖光了");
flag = false;
}else {
System.out.println(Thread.currentThread().getName() + "买到了第" + num-- + "张票");
}
}
@Override
public void run() { //synchronized拿到的是当前对象this
while (flag){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
buy();
}
}
}
synchronized块
public class Test04 {
public static void main(String[] args) {
Account account = new Account();
//分别从三家银行并发地取出若干金额
Bank bank1 = new Bank(account ,3000);
Bank bank2 = new Bank(account,6000);
Bank bank3 = new Bank(account,2000);
new Thread(bank1,"银行1").start();
new Thread(bank2,"银行2").start();
new Thread(bank3,"银行3").start();
}
}
//账户
class Account{
private int balance = 10000; //余额
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
//银行
class Bank implements Runnable{
private Account account;
private int amount; //取款金额
public Bank(Account account, int amount) {
this.account = account;
this.amount = amount;
}
@Override
public void run() {
synchronized (account) { //当前线程获得account的排它锁,其他线程只能等待
//判断余额是否满足取款金额
if ((account.getBalance() - amount) <= 0) {
System.out.println("余额不够了");
} else {
account.setBalance(account.getBalance() - amount); //修改账户余额
System.out.println("从" + Thread.currentThread().getName() + "取出" + amount);
System.out.println("账户当前还剩余" + account.getBalance());
}
}
}
}
一定要注意到底用synchronized方法还是synchronized代码块,有时候不能替换使用,弄清楚锁哪个方法哪些代码,否则很容易出问题。
Lock锁
//使用的是java.util.concurrent.locks.ReentrantLock这个类,该类实现了java.util.concurrent.locks.Lock这个接口,提供了显示定义同步锁实现互斥访问的方式
public class Test05 {
public static void main(String[] args) {
TicketSystem ticketSystem = new TicketSystem();
new Thread(ticketSystem,"小明").start();
new Thread(ticketSystem,"王老师").start();
new Thread(ticketSystem,"黄牛党").start();
}
}
class TicketSystem implements Runnable{
private int num = 10; //票数
private boolean flag = true; //是否有票
private final ReentrantLock lock = new ReentrantLock();
private void buy(){
if (num <= 0) {
System.out.println("票卖光了");
flag = false;
}else {
System.out.println(Thread.currentThread().getName() + "买到了第" + num-- + "张票");
}
}
@Override
public void run() { //synchronized拿到的是当前对象this
while (flag){
lock.lock(); //上锁
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
buy();
lock.unlock(); //解锁
}
}
}
优先使用顺序
Lock > synchronized代码块 > synchronized方法
线程同步
线程同步的实现需要依靠线程通信,在操作系统那里,我们是靠PV操作实现进程同步的,PV操作是靠申请释放资源和阻塞唤醒线程等操作实现的,因此我们也可以借鉴操作系统中进程同步实现的原理来实现线程同步。
线程通信的几个方法:
实现线程同步的示例(以生产者-消费者模式为例):
//在生产者和消费者问题中,缓冲区的访问是互斥的,生产者和消费者之间需要同步,所以是一个综合的问题,可以用同步锁的线程通信实现
public class Test01 {
public static void main(String[] args) {
BufferedArea bufferedArea = new BufferedArea();
Consumer consumer = new Consumer(bufferedArea);
Producer producer = new Producer(bufferedArea);
new Thread(consumer).start();
new Thread(producer).start();
}
}
//生产者
class Producer implements Runnable {
BufferedArea bufferedArea;
public Producer(BufferedArea bufferedArea) {
this.bufferedArea = bufferedArea;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//生产一个产品
System.out.println("生产第" + i + "只鸡");
bufferedArea.push(new Product());
}
}
}
//消费者
class Consumer implements Runnable{
BufferedArea bufferedArea;
public Consumer(BufferedArea bufferedArea) {
this.bufferedArea = bufferedArea;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//消费一个产品
System.out.println("消费了第" + i + "个产品");
bufferedArea.pop();
}
}
}
//产品
class Product{}
//缓冲区
class BufferedArea {
Product[] products = new Product[10]; //缓冲区用于暂存产品,最多能缓存10个
int num = 0; //缓冲区已缓存的产品数量
//往缓冲区放入一个产品,缓冲区只能互斥访问,所以加同步锁
public synchronized void push(Product product){
if(num == products.length){
try {
//若缓冲区已满则将生产者进程加入缓冲区的等待队列
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//若缓冲区没满则加入一个产品,并且可以唤醒正在缓冲区队列等待的消费者进程
products[num++] = product;
this.notifyAll();
}
}
//从缓冲区中取出一个产品,缓冲区只能互斥访问,所以加同步锁
public synchronized void pop(){
if(num == 0){
try {
//若缓冲区空则将消费者加入缓冲区的等待队列
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//若缓冲区没空则取出一个产品,并且可以唤醒正在缓冲区队列等待的生产进程
num--;
this.notifyAll();
}
}
}
线程池
方式一:
import java.util.concurrent.*;
public class Test03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.创建执行服务,将需要开启的线程的数量传进去
ExecutorService executorService = Executors.newFixedThreadPool(3);
//2.提交执行
Future<Boolean> future1 = executorService.submit(new ThreadPool2());
Future<Boolean> future2 = executorService.submit(new ThreadPool2());
Future<Boolean> future3 = executorService.submit(new ThreadPool2());
//3.获取返回值结果
boolean result1 = future1.get();
boolean result2 = future2.get();
boolean result3 = future3.get();
System.out.println(result1);
System.out.println(result2);
System.out.println(result3);
//4.关闭服务
executorService.shutdown();
}
}
class ThreadPool2 implements Callable<Boolean>{
@Override
public Boolean call() throws Exception {
System.out.println("线程池:" + Thread.currentThread().getName());
return false;
}
}
方式二:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test02 {
public static void main(String[] args) {
//1.开启服务,创建线程池,newFixedThreadPool中的参数为线程池的大小
ExecutorService executorService = Executors.newFixedThreadPool(3);
//2.执行
executorService.execute(new ThreadPool());
executorService.execute(new ThreadPool());
executorService.execute(new ThreadPool());
//3.关闭服务
executorService.shutdown();
}
}
class ThreadPool implements Runnable {
@Override
public void run() {
System.out.println("线程池:" + Thread.currentThread().getName());
}
}