多线程
什么是多线程
什么进程?什么是线程?
进程是一个应用程序.线程是一个进程中的执行场景/执行单元.
一个进程可以启动多个线程.
对于java程序来说,当dos命令窗口中输入:java HelloWorld回车之后
会先启动JVM,而JVM就是一个进程.
同时再启动一个垃圾回收线程负责看护,回收垃圾.
一个是垃圾回收线程,一个是执行main方法的主线程
进程可以看做是公司 而线程就是公司中的某个员工
注意:进程A和进程B的内存独立不共享
在Java语言中线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈
假设启动10个线程,会有10个栈空间,每个栈和每个栈之间互不打扰,这就是多进程并发.
实现多线程的方式
Java语言中实现线程有两种方式:
第一种方式:编写一个类,直接继承java.lang.Thread重写run方法
public class MyThread extends Thread{
public void run(){
}
}
//创建线程对象
MyThread t = new MyThread();
//启动线程
t.start();
public class ThreadTest01 {
public static void main(String[] args) {
//这里是main方法,这里的代码属于主线程,在主栈中运行.
//这里新建一个分支线程对象
MyThread mythread = new MyThread();
//启动线程
//start()方法的作用:启动一个分支线程,在JVM中开辟一个新的空间,这段代码任务完成后瞬间就结束了.
//这段代码的任务只是为了开启一个新的栈空间,只要新的空间开出来,start方法就结束了.线程就启动成功了
//启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈).
//run方法在分支栈的栈底部,main方法在主栈的栈底部.run和main是平级的.
mythread.start();
for (int i = 0;i<=100;i++ ){
System.out.println("主线程-->"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run(){
//编写程序,这段程序运行在分支线程中(分支线)
for(int i = 0;i<=100;i++){
System.out.println("分支线程-->"+i);
}
}
}
第二种方式:
编写一个类,实现java.lang.Runnable接口,实现run方法
//定义一个可运行的类
public class MyRunnable implements Runnable {
public void run(){
}
}
//创建线程对象
Thread t = new Thread (new MyRunnable());
//启动线程
t.start();
public class ThreadTest03 {
public static void main(String[] args) {
//实现多线程的第二种方式,编写一个类实现java.lang.Runnable接口
//创建一个可运行的对象
MyRunnable r = new MyRunnable();
//将可运行的对象封装成一个线程对象
Thread t = new Thread(r);
//Thread t = new Thread(new MyRunnable());//合并代码
//启动线程
t.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程-->" + i);
}
}
}
//这不是一个线程类 是一个可运行的类 他还不是一个线程
class MyRunnable implements Runnable {
@Override
public void run(){
for (int i = 0 ; i<100;i++){
System.out.println("分支线路-->"+i);
}
}
}
注意:第二种方式实现接口比较常用,因为第一个类实现了接口,它还可以去继承其他的类,更灵活
线程的生命周期

线程的状态
New 新创建
Runable 可运行 ,等待jvm调度
Blocked 被阻塞
Waiting 等待
Timed waiting 计时等待
Terminated 被终止
获取状态 : getState 方法

Thread
void join()
等待终止指定线程
void join(long millis)
等待指定的线程死亡或者经过指定的毫秒数
Thread.State getState()
示例:通过join可以等待另一个线程直到结束
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{System.out.println("hello");});
System.out.println("start");
t.start();
t.join();
System.out.println("end");
}
线程中断
Thread
void interrupt()
向线程发送中断请求,线程的中断状态设置为true,如果目前被sleep调用阻塞,那么
InterruptedException异常抛出
static boolean interrupted()
测试当前线程是否被中断,并中断状态重置为false
static boolean isInterrupted()
测试当前线程是否被中断
线程中断状态:
//方式一,通过interrupt
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1);// 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}
//方式二:通过volatiles变量
public class Main { public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
线程同步
同步编程模型:
线程t1和线程t2,在线程执行的时候,必须等待t2线程执行结束.两个线程之间发生了等待关系,这就是同步编程模型
效率较低,线程排队执行
异步编程模型:
线程t1和线程t2各自执行各自的,谁都不需要等谁,这种编程模型叫异步编程模型.其实就是多线程并发(效率较高)
不使用线程同步机制:
//银行账户
public class Account {
//账号
private String actno;
//余额
private double balance;
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
public void withdraw(double money){
//t1和t2并发这个方法
//取款之前的余额
double before =this.getBalance();
//取款之后的余额
double after = before - money;
//在这里模拟延迟,100%会出问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//t1执行到这里了,但还没来得及执行这行代码,t2线程进来withdraw方法,此时一定出问题
this.setBalance(after);
}
}
public class AccountThread extends Thread{
//两个线程必须共享同一个账户对象
private Account act;
public AccountThread(Account act){
this.act = act;
}
public void run(){
//run方法的执行表示取款操作
//假设取款5000
double money = 5000;
//取款
act.withdraw(money);
System.out.println("对账户"+act.getActno()+"取款成功.账户余额"+act.getBalance());
}
}
public class Test {
public static void main(String[] args) {
//创建账户对象(只创建一个)
Account act = new Account("act-001",10000);
//创建两个线程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
//设置name
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start();
t2.start();
}
}
t2对act-001取款成功.账户余额5000.0
t1对act-001取款成功.账户余额5000.0
//出现严重错误
使用线程同步机制--synchronized:
package threadsafe2;
//银行账户
//使用线程同步机制
public class Account {
//账号
private String actno;
//余额
private double balance;
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
public void withdraw(double money) {
//以下这几行代码必须是线程排队的,不能并发
//一个县城把这里的代码全部执行后,另一个线程才能进来
/*
线程同步机制的语法是:
synchronized(){
//线程同步代码块
}
synchronized后面括号中传递的数据是相当关键的
这个数据必须是多线程共享的数据,才能达到多线程排队
()中写什么要看想让哪些线程同步
假设t1,t2,t3,t4,t5有五个线程
只希望t1,t2,t3排队,t4,t5不需要排队,怎么办?
一定要在()中写t1,t2,t3共享的对象.而这个对象对于t4,t5来说不是共享的
这里的共享对象是账户对象
账户对象是共享的,那么this就是账户对象
不一定是this,这里只要是多线程共享的对象就行
在java语言中 任何一个对象 都是有"一把锁",其实这把锁就是一个标记(只是把他叫做锁)
100个对象100把锁 1个对象1把锁
以下代码的执行原理是:
1.假设t1 t2线程并发开始执行以下代码的时候肯定有一个先一个后
2.假设t1先执行了,遇到了synchronized,这个时候自动找"后面共享对象的对象锁"
找到之后并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的
直到同步代码块代码结束,这把锁才会释放.
3.假设t1已经占有了这把锁,此时t2也遇到了synchronized关键字,也会去占有后面
共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束
直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁后
进入同步代码块执行程序.
这样就达到了线程排队执行.
这里需要注意的是这个共享对象一定要选好.这个共享对象一定是你需要排队执行的这些线程对象所共享的
*/
synchronized (this) {
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
}
锁池:

Java中有三大变量:
实例变量:在堆中
局部变量:在栈中
静态变量:在方法区中
以上三大变量中:
局部变量永远都不会存在线程安全问题.
因为局部变量不共享(一个线程一个栈).
实例变量在堆中,堆只有一个,静态变量在方法区中,方法区只有一个.
堆和方法区都是多线程共享的,所以可能存在线程安全问题.
局部变量+常量:不会有线程安全问题
成员变量:可能会有安全问题
//取款方法
/*
在实例方法上可以使用synchronized吗?----可以
synchronized出现在实例方法上,一定锁的是this
只能是this,不能是其他的对象了.
所以这种方式不灵活
另外还有一个缺点:
synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围
导致程序的执行效率降低.所以这种方式不常用
synchronized使用在实例方法上有什么优点?
代码量少.简洁.
如果共享的对象就是this 并且需要同步的代码块就是整个方法体 建议使用这种方式
*/
public synchronized void withdraw(double money) {
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
如果使用局部变量的话:
建议使用:StringBuilder
因为局部变量不存在线程安全问题.选择StringBuilder.
StringBuffer效率比较低.
Arrylist是非线程安全的
Vector是线程安全的
HashMap HashSet是非线程安全的
HashTable是线程安全的
总结
synchronized有三种写法
第一种:同步代码块
灵活
synchronized(线程共享对象){
同步代码块
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体
第三种:在静态方法上使用synchronized
表示找类锁.
类锁永远只有一把
就算创建了100个对象,类锁也只有一把
对象锁:1个对象1把锁 100个对象100把锁
类锁:100个对象也可能只有1把锁
synchronized在开发中最好不要嵌套使用,一不小心就会导致死锁现象
线程池
FixedThreadPool:线程数固定的线程池;
CachedThreadPool:线程数根据任务动态调整的线程池;
SingleThreadExecutor:仅单线程执行的线程池
scheduledThreadPool 定期执行
守护线程
java语言中线程分为两大类:
一类是用户线程 一类是守护线程(后台线程)
其中最具有代表性的是:垃圾回收线程.
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束.
注意:主线程main方法是一个用户线程
package threadTest;
public class ThreadTest04 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据的线程");
//在启动之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
//主线程:主线程是用户线程
for(int i = 0; i<10;i++){
System.out.println(Thread.currentThread().getName()+"-->"+ i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread{
public void run(){
int i = 0;
//即使是死循环,但由于该线程是守护线程,当用户线程结束,守护线程自动终止
while(true){
System.out.println(Thread.currentThread().getName()+"-->"+(++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
定时器
定时器的作用:间隔特定的时间执行特定的程序.
每周要进行银行账户的总账操作.
每天要进行数据的备份操作.
在实际的开发中,每隔多久执行特定的一段程序,这种需求是很常见的.
在Java中可以采用多种方式实现:
可以使用sleep方法,设置睡眠时间,每到这个时间点醒来执行任务.
这种方式是最原始的定时器.
在Java类库中已经写好了一个定时器:java.util.timer,可以直接拿来用
这种方式在目前的开发中也很少用.因为现在有很多高级框架都是支持定时任务的
在实际开发中目前使用比较多的是Spring框架中的SpringTask框架.
只需要简单的配置就可以完成定时器的任务.
/*
使用定时器指定定时任务
*/
public class TimerTest {
public static void main(String[] args) throws ParseException {
//创建定时器对象
Timer timer = new Timer();
//Timer timer = new Timer(true); 守护线程的方式
//指定定时任务
// timer.schedule(定时任务,第一次执行时间,间隔多久);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime=sdf.parse("2021-07-28 15:17:01");
timer.schedule(new LogTimerTask(),firstTime,1000);
}
}
//编写一个定时任务类
//假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask {
@Override
public void run() {
//编写需要执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime+":成功完成了一次数据备份");
}
}
Callable接口
这种方式实现的线程可以获取线程的返回值.
之前讲解的那两种方式是无法获取线程的返回值的,因为run方法返回void.
系统委派一个线程去执行一个任务,该线程执行完任务之后可能会有一个执行结果,我们怎么能拿到这个执行结果呢,使用第三种方式--实现callable接口
package threadTest;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask; //JUC包下的,属于Java并发包 老JDK中没有这个包.新特性
/*
实现线程的第三种方式:
实现Callable接口
*/
public class ThreadTest05 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//第一步需要创建一个 未来任务类 对象
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception { //call方法相当于run方法.只不过这个有返回值
//线程执行一个任务 执行之后可能会有一个返回结果
//模拟执行
System.out.println("call method begin");
Thread.sleep(1000 * 5);
System.out.println("call method end");
int a = 100;
int b = 200;
return a + b; //自动装箱 (300结果变成Integar)
}
});
//创建线程对象
Thread t = new Thread(task);
//启动线程
t.start();
//这里是main方法,这是主线程中
//在主线程中怎么获取t线程的返回结果?
//get()方法的执行会导致"当前线程的阻塞"
Object obj = task.get();
System.out.println("线程执行结果:"+obj);
//main方法这里的成宿要想执行必须等待get()方法的结束
//而get()方法可能需要很久.因为get方法是为了拿另一个线程的执行结果
//而另一个线程执行是需要时间的
System.out.println("hello world!");
}
}
Future保存异步计算结果,将Future交给某个线程,然后忘掉它,计算后可以获得它
public interface Future<V>{
V get() throws ..
V get(long timeout,TimeUnit unit) throws ..;
void cancel(boolean myInterrupt)
boolean isCancelled();
boolean isDone();
}
FutureTask是一种很便利的机制,可以将Callable转为Future和Runable,实现了二者的接口
Callable<Integer> muCom = ..
FutureTask<Integer> task =t new FutureTask<Integer>(myCom)
Thread t = new Thread(task)
t.start()
submit 返回future
ExecutorService executor = Executors.newFixedThreadPool(4); // 定义任务:
Callable<String> task = new Task();
// 提交任务并获得
Future: Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果:
String result = future.get();
// 可能阻塞
Callable
V call()
Future
V get()
V get(long time,TimeUnit unit)
boolean cancel()
boolean isDone()
FutureTask(Callable task)
FutureTask(Runnable task,V result)
ThreadLocal的使用
对于多任务,Java标准库提供的线程池可以方便地执行这些任务,同时复用线程。Web应用程序就是典
型的多任务应用,每个用户请求页面时,我们都会创建一个任务,类似:
public void process(User user) {
checkPermission(user);
doWork(user);
saveStatus(user);
sendResponse(user);
}
其中每一个方法可能会调用多个方法,使用起来非常不方便,所以java提供了ThreadLocal
保存一个线程的共享数据
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
void processUser(user) {
try {
threadLocalUser.set(user);
step1();
step2();
} finally { t
hreadLocalUser.remove();
//一定要清除,因为使用了线程池,线程会重复使用
}
}//使用
void step1() {
User u = threadLocalUser.get();
log();
printUser();
}
void log() {
User u = threadLocalUser.get();
println(u.name);
}
void step2() {
User u = threadLocalUser.get();
checkUser(u.id);
}
实际上,可以把 ThreadLocal 看成一个全局 Map<Thread, Object> :每个线程获取 ThreadLocal 变
量时,总是使用 Thread 自身作为key:
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());
为了保证一定被关闭,可以封装一次,实现AutoCloseable ,调用时使用带资源的try,实际不建议采用
这种方式,因为调用代码和关闭并不在一起
public class UserContext implements AutoCloseable {
static final ThreadLocal<String> ctx = new ThreadLocal<>();
public UserContext(String user) {
ctx.set(user);
}
public static String currentUser() {
return ctx.get();
}
@Override public void close() {
ctx.remove();
}
}
//调用
try (var ctx = new UserContext("Bob")) {
// 可任意调用UserContext.currentUser():
String currentUser = UserContext.currentUser();
} // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象