1、线程概述
什么是进程?什么是线程?
进程是一个是一个应用程序(1个进程是一个软件)。
线程是一个进程中的执行场景\执行单元。
一个进程可以启动多个线程。
在Java语言中,线程A和线程B,堆内存和方法区共享。但是栈独立,一个线程一个栈。
假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。
Java中之所以有多线程机制,是为了提高程序的处理效率。
2、实现线程
Java语言中,实现线程有三种方式:
2.1 继承Thread
第一种:编写一个类,直接继承java.lang.Thread,重写run方法。
public class ThreadTeat02 {
public static void main(String[] args) {
//main方法主线程,在住栈中运行
//新建一个分支线程对象
MyTread myTread = new MyTread();
//启动线程
/*
start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,
这段代码任务完成之后,瞬间就结束了。
启动成功的线程会自动调用run()方法,并且run()方法在分支栈的底部。
run()方法在分支栈的底部,main()在主栈的底部。run和main是平级的。
*/
myTread.start();
//这里的代码还是在主线程中运行
for (int i = 0; i < 10; i++) {
System.out.println("主线程----->" + i);
}
}
}
class MyTread extends Thread {
@Override
public void run() {
//编写程序,这段程序运行在分支线程中(分支栈)
for (int i = 0; i < 10; i++) {
System.out.println("线程分支----->" + i);
}
}
}
运行结果:
程序输出结果,有先有后,有多有少。
2.2 实现Runnable接口
第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。
public class ThreadTest03 {
public static void main(String[] args) {
// MyRunnable r = new MyRunnable();
// Thread t = new Thread(r);
//合并代码
Thread t = new Thread(new MyRunnable());
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程----->" + i);
}
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("分支线程---->" + i);
}
}
}
运行结果:
第二种方式实现接口比较常用,因为一个类实现了接口还可以继承别的类。
//创建线程,采取匿名内部类的方式
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("分支线程----->"+i);
}
}
});
t.start();
2.3 实现Callable接口
这种方式实现的线程可以获取线程的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 实现Callable接口
* 这种方式的优点:可以获取线程的执行结果
* 这种方式的缺点:效率较低,在获取t线程执行结果的时候,当前线程受阻,效率较低。
*/
public class ThreadTest02 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//第一步:创建一个“未来任务类”对象
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
//线程执行一个任务,执行之后可能会有一个执行结果
System.out.println("call method begin");
Thread.sleep(1000 * 5);
System.out.println("call method over");
int a = 100;
int b = 200;
return a + b;
}
});
//创建线程对象
Thread t = new Thread(task);
//启动线程
t.start();
//在主线程main方法中获取t线程的返回结果
Object obj = task.get();
System.out.println(obj);
}
}
3、线程的生命周期
获取当前线程对象、获取线程名字、修改线程名字
//获取当前线程
Thread currentThread = Thread.currentThread();
//这个方法出现在main方法中,这个main就是当前线程
//创建线程对象
MyTread t = new MyTread();
//设置线程名字
t.setName("t0");
//获取线程名字
System.out.println(t.getName());
t.start();
运行结果:
当线程没有设置名字的时候,默认的名字有规律
Thread-0
Thread-1
Thread-2
4、sleep()
关于线程的sleep方法:
static void sleep(long millis)
1、静态方法:Thread.sleep(1000);
2、参数是毫秒
3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其他线程使用。
这行代码出现在A线程中,A线程就会进入休眠。
这行代码出现在B线程中,B线程就会进入休眠。
//当前线程休眠5秒
Thread.sleep(1000*5);
//5秒后输出 hello
System.out.println("hello");
public class ThreadTest06 {
public static void main(String[] args) {
Thread t = new MyThread06();
t.setName("t");
t.start();
try {
//在执行的时候还是会转换成:Thread.sleep(1000*5);
//这段大代码出现在main方法中,main线程休眠
t.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//5秒后输出
System.out.println("hello world");
}
}
class MyThread06 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("分支线程---->" + i);
}
}
}
4.1、终止线程的睡眠
public class ThreadTest07 {
public static void main(String[] args) {
Thread t = new Thread(new MyThread07());
t.setName("t");
t.start();
//希望5秒后中止t线程睡眠
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断t线程睡眠
//打断
t.interrupt();
}
}
class MyThread07 extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---->begin");
//run方法在父类中没有抛出任何异常
// 子类的方法不能比父类抛出给多的异常,此处必须使用try..catch
try {
//睡眠一年
Thread.sleep(1000 * 3600 * 24 * 365);
} catch (InterruptedException e) {
e.printStackTrace();
}
//一年后输出
System.out.println(Thread.currentThread().getName() + "---->over");
}
}
4.2、怎么合理的终止一个线程的执行
public class ThreadTest08 {
public static void main(String[] args) {
MyRunnable08 r = new MyRunnable08();
Thread t = new Thread(r);
t.setName("t");
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
r.run = false;
}
}
class MyRunnable08 implements Runnable {
//标记一个布尔值
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (run) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
return;
}
}
}
}
5、synchronized
什么时候数据在多线程并发的环境下会存在安全问题?
1、多线程并发
2、有共享数据
3、共享数据有修改行为
满足以上三个条件就会存在线程安全问题
怎么解决线程安全问题?
线程排队执行(不能并发)。
用排队执行解决线程安全问题,这种机制被称为:线程同步机制。
专业术语叫做,线程同步,实际上就是线程不能并发了,线程必须排队执行。
线程同步就是线程排队,线程排队就会牺牲一部分效率,但是数据安全第一位。
异步编程模型
线程t1和线程t2,各自执行各自的,谁也不需要等谁,也就是多线程并发。效率较高,但是会出现线程安全问题。
同步编程模型
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低,线程排队执行。
package com.wcs.bank;
/**
* @author wcs
* @create 2021-08-16 20:02
*/
public class Account {
private String no;
double balance;
public Account(){
}
public Account(String no, double balance) {
this.no = no;
this.balance = balance;
}
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public void withdraw(double money){
double before = this.getBalance();
double after = before-money;
this.setBalance(after);
}
}
package com.wcs.bank;
/**
* @author wcs
* @create 2021-08-16 20:11
*/
public class AccountThread extends Thread {
private Account act;
public AccountThread(Account act) {
this.act = act;
}
@Override
public void run() {
double money = 5000;
act.withdraw(money);
System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + act.getBalance());
}
}
package com.wcs.bank;
/**
* @author wcs
* @create 2021-08-16 20:16
*/
public class Test {
public static void main(String[] args) {
Account act = new Account("abc-11", 10000);
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
运行结果:
不使用线程同步机制,多线程对同一个账户取款出现线程安全问题。
要使用线程同步机制解决线程安全问题
线程同步机制的语法是:
synchronized(){
//线程同步代码块;
}
synchronized后面小括号中传的这个“数据”必须是多线程共享的数据,才能达到多线程排队。
public void withdraw(double money) {
synchronized (this) {
double before = this.getBalance();
double after = before - money;
this.setBalance(after);
}
}
synochronized有三种写法:
第一种:同步代码块
synochronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synochronized
表示共享的对象一定是this,并且同步代码块是整个方法体。
第三种:在静态方法上使用synichronized
表示找类锁
类锁永远只有一把。
6、ReentrantLock
Java在过去很长一段时间只能通过 synchronized 关键字来实现互斥,它有一些缺点。比 如你不能扩展锁之外的方法或者块边界,尝试获取锁时不能中途取消等。Java 5 通过 Lock 接口提供了更复杂的控制来解决这些问题。 ReentrantLock 类实现了 Lock,它拥有与synchronized 相同的并发性和内存语义且它还具有可扩展性。还有一个最主要的就是ReentrantLock还可以实现公平锁机制。什么叫公平锁呢?也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
package com.wcs;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* ReentrantLock
*/
public class LockTest {
static Lock lock = new ReentrantLock();
//true 公平锁 每个线程执行一次
// static Lock lock = new ReentrantLock(true);
static void m1() {
for (int i = 0; i < 5; i++) {
lock.lock();
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "--->" + i);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
new Thread(() -> m1(), "t1").start();
new Thread(() -> m1(), "t2").start();
new Thread(() -> m1(), "t3").start();
}
}
7、死锁概述
/**
* synchronized最好不要嵌套使用
* 容易发生死锁现象
*/
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new MyThread1(o1, o2);
Thread t2 = new MyThread1(o1, o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread {
Object o1;
Object o2;
public MyThread1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1) {
synchronized (o2) {
}
}
}
}
class MyThread2 extends Thread {
Object o1;
Object o2;
public MyThread2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2) {
synchronized (o1) {
}
}
}
}
8、开发中应该怎么解决线程安全问题
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。
9、守护线程
Java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意:主线程main方法是一个用户线程。
/**
* 守护线程
*/
public class ThreadTest01 {
public static void main(String[] args) {
Thread t = new MyThread();
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 MyThread extends Thread {
@Override
public void run() {
int i = 0;
while (true) {
System.out.println(Thread.currentThread().getName() + "-->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
10、定时器
定时器的作用:
间隔特点的时间,执行特定的程序。
在Java的类库中已经写好了一个定时器:java.util.Timer。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* 使用定时器指定定时任务
*/
public class TimerTest {
public static void main(String[] args) throws ParseException {
//创建定时对象
Timer timer = new Timer();
//指定定时任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2021-8-17 10:20:00");
//timer.schedule(定时任务,第一次执行时间,间隔多久执行一次)
timer.schedule(new LogTimerTask(), firstTime, 1000 * 10);
}
}
class LogTimerTask extends TimerTask {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date;
String strTime = sdf.format(new Date());
System.out.println(strTime + "成功完成了一次数据备份!");
}
}
运行结果:
11、关于Object类中的wait和notify方法(生产者和消费者模式)
wait和notify方法不是线程对象的方法,是Java中任何一个Java对象都有的方法,因为这两个是object类中自带的。
wait()方法的作用?
Object o = new Object();
o.wait();
表示让正在o对象上活动的线程进入等待状态,无限期等待,直到被唤醒为止。
notify方法的作用?
Object o = new Object();
o.notify();
表示唤醒正在o对象上等待的线程。
还有一个notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程。
import java.util.ArrayList;
import java.util.List;
/**
* 使用wait方法和notify方法实现“生产者和消费者模式”
*/
public class ThreadTest03 {
public static void main(String[] args) {
// 创建1个仓库对象,共享的。
List list = new ArrayList();
// 创建两个线程对象
// 生产者线程
Thread t1 = new Thread(new Producer(list));
// 消费者线程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
// 生产线程
class Producer implements Runnable {
// 仓库
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直生产(使用死循环来模拟一直生产)
while (true) {
// 给仓库对象list加锁。
synchronized (list) {
if (list.size() > 0) { // 大于0,说明仓库中已经有1个元素了。
try {
// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到这里说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒消费者进行消费
list.notifyAll();
}
}
}
}
// 消费线程
class Consumer implements Runnable {
// 仓库
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直消费
while (true) {
synchronized (list) {
if (list.size() == 0) {
try {
// 仓库已经空了。
// 消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到此处说明仓库中有数据,进行消费。
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒生产者生产。
list.notifyAll();
}
}
}
}
12、多线程工具类
12.1、CountDownLatch
package com.wcs;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* CountDownLatch
* void await() 使当前线程等待直到锁向下计数为零,除非线程 interrupted。
* void countDown() 减少锁的数量,释放所有等待的线程,如果计数为零。
*/
public class CountDownLatchTest01 {
static List<String> list = new ArrayList<>();
static CountDownLatch latch = new CountDownLatch(1);
public static void add() {
System.out.println(Thread.currentThread().getName() + "添加线程启动");
while (true) {
try {
Thread.sleep(1000);
list.add("hello");
//添加一个元素size=1,获取下标为1的元素却是第二个元素,所以要-1
System.out.printf("%s %d%n", list.get(list.size() - 1), list.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5) {
//当元素为5个的时候打开门栓
latch.countDown();
}
}
}
public static void check() {
System.out.println(Thread.currentThread().getName() + "监控线程启动");
try {
//线程开启后一直在就绪等待,直到门栓打开
latch.await();
System.out.println("已经有五个元素了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//T1线程 添加元素 元素有5个时候,打开门栓告诉T2线程
new Thread(() -> add(), "T1").start();
//T2线程 监控元素
new Thread(() -> check(), "T2").start();
}
}
package com.wcs;
import java.util.concurrent.CountDownLatch;
/**
* 多线程启动,有一个线程收尾
*/
public class CountDownLatchTest02 {
static CountDownLatch latch = new CountDownLatch(5);
static void work() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "--->" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
latch.countDown();
}
static void over() {
System.out.println("准备收尾");
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("收尾成功");
}
public static void main(String[] args) {
new Thread(() -> work(), "T1").start();
new Thread(() -> work(), "T2").start();
new Thread(() -> work(), "T3").start();
new Thread(() -> work(), "T4").start();
new Thread(() -> work(), "T5").start();
new Thread(() -> over(), "over").start();
}
}
12.2、CyclicBarrier
package com.wcs;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
*三人到齐就开饭
*/
public class CyclicBarrierTest {
static CyclicBarrier bar = new CyclicBarrier(3,()-> System.out.println("大家都到齐了,开饭吧"));
static void eat(){
String tn = Thread.currentThread().getName();
Random rand = new Random();
int time = rand.nextInt(30);
System.out.printf("%s 准备动身,预计%d秒后到达%n",tn,time);
try {
TimeUnit.SECONDS.sleep(time);
System.out.printf("%s 到了%n",tn);
if ("王总".equals(tn)){
bar.await(5,TimeUnit.SECONDS);
}
bar.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(CyclicBarrierTest::eat,"张三").start();
new Thread(CyclicBarrierTest::eat,"Jack").start();
new Thread(()->eat(), "李四").start();
new Thread(()->eat(), "王总").start();
}
}
CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程。与 CyclicBarrier 不同的是,CountdownLatch 不能重新使用。
13、线程池
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且 一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来 响应处理,它们被称为线程池,里面的线程叫工作线程。从 JDK1.5 开始,Java API 提供 了 Executor 框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目 固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池
*/
public class TestPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(5);
service.execute(new MyThreadPool());
service.execute(new MyThreadPool());
service.execute(new MyThreadPool());
service.execute(new MyThreadPool());
service.shutdown();
}
}
class MyThreadPool implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
都可以用来让一组线程等待其它线程。与 CyclicBarrier 不同的是,CountdownLatch 不能重新使用。
13、线程池
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且 一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来 响应处理,它们被称为线程池,里面的线程叫工作线程。从 JDK1.5 开始,Java API 提供 了 Executor 框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目 固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池
*/
public class TestPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(5);
service.execute(new MyThreadPool());
service.execute(new MyThreadPool());
service.execute(new MyThreadPool());
service.execute(new MyThreadPool());
service.shutdown();
}
}
class MyThreadPool implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}