java多线程
1、线程简介
栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM 的启动参数来进行调整,栈空间用光了会引发 StackOverflowError,而堆和常量池空间不足则会引发 OutOfMemoryError。
String str = new String("hello");
上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而 “hello” 这个字面量是放在方法区的。
例子:
开车 + 打电话
吃饭 + 玩手机
这些动作都可以抽象为任务,虽然看起来一心二用,但人只有一个大脑,在一个时间片刻只能处理一个任务。
CPU 也是一样,面对多个任务,只能在一个时间片刻处理一个任务。
主线程调用 run 方法和调用 start 方法开启子线程的区别如下图所示。
-
线程就是独立的执行路径;
-
在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,GC 线程;
-
main()称之为主线程,为系统的入口,用于执行整个程序;
-
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预。
-
对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
-
线程会带来额外的开销,如 CPU 调度时间,并发控制开销。
-
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
2、线程实现
线程的三种实现方式:
2.1 继承 Thread 类,重写 run 方法
继承 Thread 类,重写 run 方法。创建这个类的对象,再调用 start() 即可
package com.sjmp.Thread01;
/**
* @ClassName ThreadTest
* @Description TODO
* @Author sjmp1573
* @Date DATE{TIME}
*/
public class ThreadTest {
// 继承 Thread 类并重写 run 方法
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println("I am a child thread");
}
}
public static void main(String[] args) {
//创建一个线程
MyThread thread = new MyThread();
//启动线程
thread.start();
}
}
下载文件需要在 pom.xml 中 commons io 包。
使用该方法下载网络图片。
package com.sjmp.demo01;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
* @author: sjmp1573
* @date: 2020/11/15 20:58
* @description:
*/
public class TestThread2 extends Thread{
// 网络图片地址
private String url;
// 保存的文件名
private String name;
public TestThread2(String url,String name){
this.url = url;
this.name = name;
}
@Override
public void run() {
// 进入线程后,会创建一个下载器,下载器通过 downloader 方法,传入 url 和 name 下载相应的资源
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("下载了文件名为:"+ name);
}
public static void main(String[] args) {
// 这是 TestThread2 类的主方法
// 创建三个继承 Thread 的子类
TestThread2 test01 = new TestThread2("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796445054,4193265240&fm=26&gp=0.jpg", "test01");
TestThread2 test02 = new TestThread2("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1605454961548&di=c3b49cc5869f058a6cded1434ea56f85&imgtype=0&src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F5%2F538ec3134b63b.jpg", "test02");
TestThread2 test03 = new TestThread2("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2044644877,1766802492&fm=15&gp=0.jpg", "test03");
// 并开启线程
test01.start();
test02.start();
test03.start();
}
}
//下载器,这是一个类
class WebDownloader{
// 下载方法
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader 方法出现问题");
}
}
}
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
2.2 继承 Runnable 接口,创建 Tread 对象
继承 Runnable 接口,创建 Tread 对象,传入实现类,开启 start 方法.
package com.sjmp.Thread01;
/**
* @ClassName ThreadRunnableTest
* @Description TODO
* @Author sjmp1573
* @Date DATE{TIME}
*/
public class ThreadRunnableTest {
public static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("I am a child thread --Runnable");
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
new Thread(thread).start();
new Thread(thread).start();
}
}
以上两种方式的比较:
继承 Thread 类
- 子类继承 Thread 类具备多线程能力
- 启动线程:子类对象 .start()
- 不建议使用:避免 OOP 单继承局限性
实现 Runnable 接口
- 实现接口 Runnable 具有多线程能力
- 启动线程:传入目标对象+Thread对象.start()
- 推荐使用:避免单继承局限性,方便同一个对象被多个线程使用。
火车抢票实例:
Runnable 实现多线程,创造一个实列 ticketRunnable ,可共享给多个线程。
package com.sjmp.demo01;
/**
* @author: sjmp1573
* @date: 2020/11/15 21:45
* @description:
*/
// 多个线程同时操作同一个对象
// 买火车票的例子
// 发现问题:多个线程操作同一个资源,线程不安全,数据紊乱!
public class TicketRunnable implements Runnable{
private int ticketNums = 10;
@Override
public void run() {
while (true){
if (ticketNums<=0){
break;
}
// 模拟延时
/*
IllegalArgumentException
if the value of {@code millis} is negative, or the value of
{@code nanos} is not in the range {@code 0-999999}
InterruptedException
if any thread has interrupted the current thread. The
<i>interrupted status</i> of the current thread is
cleared when this exception is thrown.
*/
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketNums--;
System.out.println(Thread.currentThread().getName()+"-->拿到了第"+ticketNums+"票");
}
}
public static void main(String[] args) {
// 实现了 Runnable 接口的类,创建其实例
TicketRunnable ticketRunnable = new TicketRunnable();
// ticketRunnable 实例可用于多个线程,其中的资源被共享。
new Thread(ticketRunnable,"01小明+++++").start();
new Thread(ticketRunnable,"02老师-----").start();
new Thread(ticketRunnable,"03黄牛=====").start();
}
}
"D:\Program Files (x86)\Java\bin\java.exe" "-javaagent:D:\Program Files (x86)\IDEA\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=64250:D:\Program Files (x86)\IDEA\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath "D:\Program Files (x86)\Java\jre\lib\charsets.jar;D:\Program Files (x86)\Java\jre\lib\deploy.jar;D:\Program Files (x86)\Java\jre\lib\ext\access-bridge-64.jar;D:\Program Files (x86)\Java\jre\lib\ext\cldrdata.jar;D:\Program Files (x86)\Java\jre\lib\ext\dnsns.jar;D:\Program Files (x86)\Java\jre\lib\ext\jaccess.jar;D:\Program Files (x86)\Java\jre\lib\ext\jfxrt.jar;D:\Program Files (x86)\Java\jre\lib\ext\localedata.jar;D:\Program Files (x86)\Java\jre\lib\ext\nashorn.jar;D:\Program Files (x86)\Java\jre\lib\ext\sunec.jar;D:\Program Files (x86)\Java\jre\lib\ext\sunjce_provider.jar;D:\Program Files (x86)\Java\jre\lib\ext\sunmscapi.jar;D:\Program Files (x86)\Java\jre\lib\ext\sunpkcs11.jar;D:\Program Files (x86)\Java\jre\lib\ext\zipfs.jar;D:\Program Files (x86)\Java\jre\lib\javaws.jar;D:\Program Files (x86)\Java\jre\lib\jce.jar;D:\Program Files (x86)\Java\jre\lib\jfr.jar;D:\Program Files (x86)\Java\jre\lib\jfxswt.jar;D:\Program Files (x86)\Java\jre\lib\jsse.jar;D:\Program Files (x86)\Java\jre\lib\management-agent.jar;D:\Program Files (x86)\Java\jre\lib\plugin.jar;D:\Program Files (x86)\Java\jre\lib\resources.jar;D:\Program Files (x86)\Java\jre\lib\rt.jar;E:\SJMP\SpringProject\out\production\LeetCode" com.sjmp.TicketRunnable
03黄牛=====-->拿到了第8票
01小明+++++-->拿到了第8票
02老师------->拿到了第8票
03黄牛=====-->拿到了第7票
02老师------->拿到了第7票
01小明+++++-->拿到了第7票
02老师------->拿到了第6票
03黄牛=====-->拿到了第6票
01小明+++++-->拿到了第6票
01小明+++++-->拿到了第5票
03黄牛=====-->拿到了第4票
02老师------->拿到了第5票
03黄牛=====-->拿到了第3票
02老师------->拿到了第3票
01小明+++++-->拿到了第3票
02老师------->拿到了第1票
03黄牛=====-->拿到了第2票
01小明+++++-->拿到了第2票
02老师------->拿到了第0票
01小明+++++-->拿到了第-1票
03黄牛=====-->拿到了第-1票
Process finished with exit code 0
2.3 实现 Callable 接口(了解)
- 实现 Callable 接口,需要返回值类型
- 重写 call 方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService = Executor.newFixedThreadPool(1);
- 提交执行:Future result1 = ser.submit(1);
- 获取结果:boolean r1 = result.get()
- 关闭服务:ser.shutdownNow():
package com.sjmp.demo01;
import java.util.concurrent.*;
/**
* @author: sjmp1573
* @date: 2020/11/15 22:16
* @description:
*/
public class ThreadByCallable implements Callable<Boolean> {
// 网络图片地址
private String url;
// 保存的文件名
private String name;
public ThreadByCallable(String url,String name){
this.url = url;
this.name = name;
}
@Override
public Boolean call() throws Exception {
// 进入线程后,会创建一个下载器,下载器通过 downloader 方法,传入 url 和 name 下载相应的资源
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("下载了文件名为:"+ name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 这是 TestThread2 类的主方法
// 创建三个继承 Thread 的子类
ThreadByCallable test01 = new ThreadByCallable("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796445054,4193265240&fm=26&gp=0.jpg", "test01");
ThreadByCallable test02 = new ThreadByCallable("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1605454961548&di=c3b49cc5869f058a6cded1434ea56f85&imgtype=0&src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F5%2F538ec3134b63b.jpg", "test02");
ThreadByCallable test03 = new ThreadByCallable("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2044644877,1766802492&fm=15&gp=0.jpg", "test03");
// 创建执行服务:
ExecutorService service = Executors.newFixedThreadPool(3);
// 提交执行
Future<Boolean> submit01 = (Future<Boolean>) service.submit(test01);
Future<Boolean> submit02 = (Future<Boolean>) service.submit(test02);
Future<Boolean> submit03 = (Future<Boolean>) service.submit(test03);
boolean rs1 = submit01.get();
boolean rs2 = submit02.get();
boolean rs3 = submit03.get();
// 关闭服务
service.shutdownNow();
}
}
//下载器,这是一个类
class WebDownloader{
// 下载方法
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader 方法出现问题");
}
}
}
以下代码来源于《Java 并发编程之美》
CallableTest 类实现了 Callable 接口的 call() 方法。在 main() 函数内首先创建了一个 FutureTask 对象(构造函数为 CallableTest 实例),然后使用创建的 FutureTask 对象作为任务创建了一个线程并且启动它,最后通过 futureTask.get() 等待任务执行完毕返回结果。
public class FutureTask<V> implements RunnableFuture<V>{}
public interface RunnableFuture<V> extends Runnable, Future<V>
package com.sjmp.practice;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author: sjmp1573
* @date: 2021/4/20 21:12
* @description:
*/
public class CallableTest implements Callable<String> {
@Override
public String call() throws Exception {
return "Hello CallableThread";
}
public static void main(String[] args) {
CallableTest callableTest = new CallableTest();
FutureTask<String> futureTask = new FutureTask<>(callableTest);
new Thread(futureTask).start();
try {
String s = futureTask.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
Hello CallableThread
Process finished with exit code 0
2.4 Lamda 表达式
Lamda 表达式属于函数式编程的概念
(paraems) -> expressionp[表达式]
(params) -> statement[语句]
(params) -> {statements}
a->System.out.println("i like lamda-->"+a);
new Thread(()->System.out.println("多线程学习...")).start();
- 理解 Functional Interface(函数式接口)是学习 Java8 Lambda 表达式的关键所在。
- 函数式接口的定义:
- 任何接口,如果只包含唯一一个抽象方法,那么它就是函数式接口。
- 对于函数式接口,可以通过 Lamda 表达式来创建该接口的对象。
Lamda 表达式的演进:
package com.sjmp.demo02;
/**
* @author: sjmp1573
* @date: 2020/11/16 20:11
* @description:
*/
public class LamdaExpression {
// 3.2 实现函数式接口的第二种方法,静态内部类
static class Like2 implements ILike{
@Override
public void lamda() {
System.out.println("------3.2 静态内部类实现函数式接口-----");
}
}
public static void main(String[] args) {
// 3.1 实现函数式接口的第一种方法
ILike like1 = new Like1();
like1.lamda();
System.out.println("--3.1 普通方法实现函数式接口--");
// 3.2 实现函数式接口的第二种方法,静态内部类
new Like2().lamda();
// 3.3 局部内部类实现函数式接口
class Like3 implements ILike{
@Override
public void lamda() {
System.out.println("------3.3 局部内部类实现函数式接口--------");
}
}
new Like3().lamda();
// 3.4 匿名内部类实现函数式接口
new ILike() {
@Override
public void lamda() {
System.out.println("------3.4 匿名内部类实现函数式接口----------");
}
}.lamda();
// 3.5 lamda 表达式实现函数式接口
ILike like5 = ()->{
System.out.println("--3.5 lamda 表达式实现函数式接口--");
};
like5.lamda();
}
}
// 1. 定义一个函数式接口
interface ILike{
void lamda();
}
// 2. 实现类
class Like1 implements ILike{
@Override
public void lamda() {
}
}
2.5 静态代理模式
多线程 Thread 为代理,Runnable 为被代理对象:
package com.sjmp.demo02;
/**
* @author: sjmp1573
* @date: 2020/11/16 19:32
* @description:
*/
//这是代理
public class StaticProxy implements Marry{
private Marry you;
public StaticProxy(You you){
this.you = you;
}
public static void main(String[] args) {
// Runnable 是被代理的对象,Thread 是代理
/*
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("----结婚----");
}
}).start();
*/
// 使用 lamda 表达式
new Thread(()->{
System.out.println("----结婚----");
}).start();
new StaticProxy(new You()).HappyMarry();
}
@Override
public void HappyMarry() {
doBefore();
you.HappyMarry();
doAfter();
}
public static void doBefore(){
System.out.println("-----婚前布置------");
}
public static void doAfter(){
System.out.println("-------婚后收钱-----");
}
}
interface Marry{
void HappyMarry();
}
//真实角色,Marry
class You implements Marry{
@Override
public void HappyMarry() {
System.out.println("----- 被代理人 开始结婚------");
}
}
3、线程的 5 种状态
- 创建
- 就绪
- 阻塞
- 运行
- 死亡
3.1 线程的一些常用方法
线程的一些方法如下图所示:
3.1.1 线程休眠——sleep()
- sleep(时间)指定当前线程阻塞的毫秒数;
- sleep 存在异常 InterruptedException;
- sleep 时间达到后线程进入就绪状态;
- sleep 可以模拟网络延时,倒计时等;
- sleep 每一个对象都有一个锁,sleep 不会释放锁;
sleep() 方法的用处
package com.sjmp.method;
import java.awt.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import static java.lang.Thread.*;
/**
* @author: sjmp1573
* @date: 2020/11/16 21:50
* @description:
*/
public class TestSleep {
public static void main(String[] args) throws InterruptedException {
// Thread.sleep() 用于倒计时
// tenStop();
// 打印当前系统时间
Date date = new Date(System.currentTimeMillis());
boolean flag = true;
int i = 5;
while(flag){
if(--i<=0){
flag = false;
}
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
date = new Date(System.currentTimeMillis());//更新时间
}
}
// 写一个倒计时的方式
public static void tenStop() throws InterruptedException {
int num = 10;
while(true){
try{
sleep(1000);
}catch ( InterruptedException e){
e.printStackTrace();
}
if (num<=0){
break;
}
System.out.println(num--);
}
}
}
3.1.2 线程礼让——yield()
- 礼让线程,让当前正在执行的线程暂停,但不阻塞;
- 将线程从运行状态转为就绪状态;
- 让 CPU 从新调度,有可能还是调度该礼让线程。
package com.sjmp.method;
/**
* @author: sjmp1573
* @date: 2020/11/16 22:22
* @description:
*/
public class TestYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开启了线程");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"结束了线程");
}
public static void main(String[] args) {
TestYield testYield = new TestYield();
Thread threadA = new Thread(testYield,"threadA");
Thread threadB = new Thread(testYield,"threadB");
threadA.start();
threadB.start();
}
}
3.1.23合并线程——Join()
Join 合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞。
可以想象成插队。
package com.sjmp.demo02;
/**
* @author: sjmp1573
* @date: 2020/11/16 21:33
* @description:
*/
public class ThreadStop implements Runnable{
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
System.out.println("--- ThreadStop ---"+i);
i++;
}
}
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
ThreadStop threadStop = new ThreadStop();
Thread thread = new Thread(threadStop);
thread.start();
for (int i = 0; i < 1000; i++) {
if (i==900){
threadStop.stop();
}
System.out.println("--- main ---"+i);
}
}
}
3.2 停止线程的方式
- 不推荐使用 JDK 提供的 stop ()、destroy()方法。【已弃用】
- 推荐线程自己停止下来
- 建议使用一个标志位进行终止变量 , 当 flag == false,则终止线程运行。
package com.sjmp.demo02;
/**
* @author: sjmp1573
* @date: 2020/11/16 21:33
* @description:
*/
public class ThreadStop implements Runnable{
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
System.out.println("--- ThreadStop ---"+i);
i++;
}
}
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
ThreadStop threadStop = new ThreadStop();
Thread thread = new Thread(threadStop);
thread.start();
for (int i = 0; i < 1000; i++) {
if (i==900){
threadStop.stop();
}
System.out.println("--- main ---"+i);
}
}
}
3.3 线程状态观测
Thread.State
线程状态。线程可以处于以下状态之一:
- NEW
尚未启动的线程处于此状态。 - RUNNABLE
在 Java 虚拟机中执行的线程处于此状态。 - BLOCKED
被阻塞等待监视器锁定的线程处于此状态。 - WAITING
正在等待另一个线程执行特定动作的线程处于此状态。 - TERMINATED
已退出的线程处于此状态。
一个线程可以在给定时间点处于一个状态。
package com.sjmp.method;
/**
* @author: sjmp1573
* @date: 2020/11/17 21:10
* @description:
*/
public class TestState{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
System.out.println(i);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread 执行结束了!");
});
Thread.State state = thread.getState();
System.out.println(state);
thread.start();
System.out.println(thread.getState());
System.out.println(" 我开始循环了 ");
while(state != Thread.State.TIMED_WAITING){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
state = thread.getState();
System.out.println(state);
}
}
}
3.4 线程优先级
- Java 提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
- 线程的优先级用数字表示,范围从1~10.
Thread.MIN_PRIORITY=1;
Thread.MAX_PRIORITY=10;
Thread.NORM_PRIORITY=5; - 使用以下方式改变或获取优先级
getPriority().setPriority(int xxx)
优先级的设定建议在 start() 调度前
package com.sjmp.method;
/**
* @author: sjmp1573
* @date: 2020/11/17 21:32
* @description:
*/
public class TestPriority implements Runnable {
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getName());
}
public static void main(String[] args) {
TestPriority runnable = new TestPriority();
Thread thread01 = new Thread(runnable,"01");
Thread thread02 = new Thread(runnable,"02");
Thread thread03 = new Thread(runnable,"03");
Thread thread04 = new Thread(runnable,"04");
Thread thread05 = new Thread(runnable,"05");
thread01.setPriority(Thread.MAX_PRIORITY);
thread02.setPriority(7);
thread03.setPriority(6);
thread04.setPriority(5);
thread05.setPriority(4);
thread01.start();
thread02.start();
thread03.start();
thread04.start();
thread05.start();
}
}
3.5 守护(daemon)线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如,后台记录操作日志,监控内存垃圾回收等待…
package com.sjmp.method;
/**
* @author: sjmp1573
* @date: 2020/11/17 21:43
* @description:
*/
public class TestsetDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
thread.setDaemon(true);
thread.start();
new Thread(you).start();
}
}
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("Daemoning...");
}
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 365; i++) {
System.out.println("living...");
}
System.out.println("game over---------------------");
}
}
3.6 并发
同一个对象被多个线程同时操作
现实生活中,我们会遇到”同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队一个个来。
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象.这时候我们就需要线程同步.线程同步其实就是一种等待机制,多个需要同时访问!此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
形成线程安全的条件:
队列和锁
4、线程同步
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可.存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题.
4.1 线程不安全举例
举例:不安全的售票
package com.sjmp.Concurrent;
/**
* @author: sjmp1573
* @date: 2020/11/17 22:03
* @description:
*/
public class UnsafeBuyTicket {
}
class BuyTicket implements Runnable{
private int ticketNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag){
buy();
}
System.out.println("售罄");
}
//加关键字 synchronized 就可以变成线程安全的
public void buy(){
if (ticketNum<=0){
flag = false;
return;
}
try {
// 模拟买票延时
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" get ticket:"+ticketNum);
ticketNum--;
}
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread thread01 = new Thread(buyTicket,"01");
Thread thread02 = new Thread(buyTicket,"02");
Thread thread03 = new Thread(buyTicket,"03");
thread01.start();
thread02.start();
thread03.start();
}
}
举例:银行取钱
代码省略
举例:线程不安全的集合
可参考:ArrayList为什么是线程不安全的:https://blog.csdn.net/qq_42183409/article/details/100586255
package com.sjmp.Concurrent;
import java.util.ArrayList;
/**
* @author: sjmp1573
* @date: 2020/11/17 22:22
* @description:
*/
public class UnsafeList {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
举例:线程安全的集合:CopyOnWriteArrayList
package com.sjmp.Concurrent;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author: sjmp1573
* @date: 2020/11/18 9:48
* @description:
*/
public class TestJUC {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
4.2 同步方法
- 由于我们可以通过关键字 private 关键字来保证数据对象只能被方法访问,所以我们只要针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种方法:
synchronized 方法和 synchronized 块.
同步方法:
public synchronized void method(int args){}
- synchronized 方法控制 “对象” 的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放,后面被阻塞的线程才能获得这个锁,继续执行。
缺陷:若将一个大的方法申明为 synchronized 将会影响效率。
4.3 同步块
同步块:synchronized(Obj){}
Obj 称之为同步监视器
- Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是 this ,就是这个对象本身,或者是 class
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中的代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
4.3 死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥“两个以上对象的锁”时,就可能会发生“死锁”的问题。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上述四个条件,只要破坏其任意一个就可避免死锁的发生。
4.4 Lock(锁)
- 从 JDK 5.0 开始,Java 提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用 Lock对象充当
- java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock ,可以显示加锁、释放锁。
synchronized 与 Lock 的对比
- Lock 是显示锁(手动开启和关闭),synchronized 是隐式锁,出了作用域自动释放
- Lock 只有代码加锁,synchronized 有代码块锁和方法锁
- 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并具有更好的扩展性(提供更多的子类)
- Lock > 同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
package com.sjmp.Concurrent;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: sjmp1573
* @date: 2020/11/18 16:52
* @description:
*/
public class TestLock {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket).start();
new Thread(ticket).start();
new Thread(ticket).start();
}
}
class Ticket extends Thread{
private int ticketNums = 10;
// 定义 lock 锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
lock.lock();
if (ticketNums>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else{
break;
}
}finally {
lock.unlock();
}
}
}
}
5.线程通信
应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
- 如果仓库中没有产品,则将生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费,直到仓库中再次放入产品为止。
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又需要马上通知消费者消费
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费.
- 在生产者消费者问题中,仅有synchronized是不够的
synchronized 可阻止并发更新同一个共享资源,实现了同步
synchronized 不能用来实现不同线程之间的消息传递(通信)
5.1 解决线程之间通信问题的几个方法
注意:均是 Object 类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常 llegalMonitorStateException
sleep 与 wait 的区别可参考链接:https://www.xuexila.com/baikezhishi/537124.html
5.2 解决线程之间通信的方式1:管程法
并发写作模型“生产者/消费者模式”–>管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个缓冲区
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
package com.sjmp.advanced;
/**
* @author: sjmp1573
* @date: 2020/11/18 20:52
* @description:
*/
// 生产者,消费者,产品,缓冲区
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
// 生产者
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container){
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("生产了"+i+"只鸡");
container.push(new Chicken(i));
}
}
}
class SynContainer{
// 需要一个容器的大小
Chicken[] chickens = new Chicken[10];
// 容器计数器
int count = 0;
// 生产者放入产品
public synchronized void push(Chicken chicken){
// 如果容器满了,就需要等待消费者消费
if (count == chickens.length){
// 通知消费者消费,生产等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
chickens[count] = chicken;
count++;
this.notifyAll();
}
// 消费者消费产品
public synchronized Chicken pop(){
// 判断能否消费
if(count==0){
// 等待生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果可以消费
count--;
Chicken chicken = chickens[count];
// 可以通知消费了
this.notifyAll();
return chicken;
}
}
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了-->"+container.pop().id+"只鸡");
}
}
}
// 产品
class Chicken{
int id; //产品编号
public Chicken(int id){
this.id = id;
}
}
5.3 解决线程之间通信的方式1:信号灯法
package com.sjmp.advanced;
/**
* @author: sjmp1573
* @date: 2020/11/18 21:34
* @description:
*/
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Wathcher(tv).start();
}
}
//生产者--演员
class Player extends Thread{
TV tv;
public Player(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("天天向上");
}
}
}
}
//观众
class Wathcher extends Thread{
TV tv;
public Wathcher(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//产品--节目
class TV{
// 演员表演,观众等待 T
// 观众观看,演员等待 F
String voice; // 表演节目
boolean flag = true;
// 表演
public synchronized void play(String voice){
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了: "+voice);
// 通知观众观看
this.notifyAll();// 通知唤醒
this.voice = voice;
this.flag = !flag;
}
// 观看
public synchronized void watch(){
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了: "+voice);
// 通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
5.4 使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
优点:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理…
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间会终止
package com.sjmp.advanced;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author: sjmp1573
* @date: 2020/11/18 21:53
* @description:
*/
public class TestPool {
public static void main(String[] args) {
// 1.创建服务,创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// newFixedThreadPool 参数为线程池大小
// 执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
// 2.关闭连接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
6 补充内容
package com.sjmp.Thread01;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @ClassName ThreadFutureTest
* @Description TODO
* @Author sjmp1573
* @Date DATE{TIME}
*/
public class ThreadFutureTest {
public static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception {
return "Thread-Callable- hello";
}
}
public static void main(String[] args) {
// 创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
new Thread(futureTask).start();
try {
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如上代码中的 CallerTask 类实现了 Callable 接口的 call() 方法。在 main 函数内首先创建了一个 FutrueTask 对象(构造函数为 CallerTask 的实例),然后使用创建的 FutrueTask 对象作为任务创建了一个线程并且启动它,最后通过futureTask.get() 等待任务执行完毕并返回结果。
小结:使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递,而如果使用 Runnable 方式,则只能使用主线程里面被声明为 final 的变量。不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么子类不能再继承其他类,而 Runable 则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是 Futuretask 方式可以。
6.1 wait() 方法
w MyThread());
// 2.关闭连接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
# 6 补充内容
~~~java
package com.sjmp.Thread01;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @ClassName ThreadFutureTest
* @Description TODO
* @Author sjmp1573
* @Date DATE{TIME}
*/
public class ThreadFutureTest {
public static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception {
return "Thread-Callable- hello";
}
}
public static void main(String[] args) {
// 创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
new Thread(futureTask).start();
try {
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如上代码中的 CallerTask 类实现了 Callable 接口的 call() 方法。在 main 函数内首先创建了一个 FutrueTask 对象(构造函数为 CallerTask 的实例),然后使用创建的 FutrueTask 对象作为任务创建了一个线程并且启动它,最后通过futureTask.get() 等待任务执行完毕并返回结果。
小结:使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递,而如果使用 Runnable 方式,则只能使用主线程里面被声明为 final 的变量。不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么子类不能再继承其他类,而 Runable 则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是 Futuretask 方式可以。