一、概念
1、线程与进程
进程: 一个软件,运行了,就是一个进程。
线程:一个软件运行了,可以同时干很多事情,每个事情就是一个线程。QQ可以聊天同时也可以传文件,两者同时进行。
2、 同一个CPU在同一时间其实就只干一件事儿。(核)
如果电脑是单核的,支持不支持多线程呢? 支持。 多个线程抢占CPU执行权,抢到就执行,执行完就释放,继续抢。导致有一个错觉,就是同时进行的。
二、创建多线程的两种方式
1、继承Thread类
package com.qfedu.day13;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/3/17 10:10
* @Version 1.0
*
* 1、第一种创建多线程的方式 继承 Thread ,重写run 方法
* 2、run 方法中的代码就是多线程需要执行的代码
*/
public class MyThread extends Thread{
/**
* 每一个方法都有一个无参数的构造方法,如果是继承,会自动调用父类的无参构造
*/
public MyThread(){
super();
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
/**
* public Thread() {
* init(null, null, "Thread-" + nextThreadNum(), 0);
* }
*/
System.out.println(Thread.currentThread().getName()+"******"+i);
}
}
}
package com.qfedu.day13;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/3/17 10:13
* @Version 1.0
*/
public class TestThread {
/**
* 目前这个代码是三个线程同时执行,谁先执行谁后执行,都未可知
* 跟之前不一样,以前是只有一个Main线程(主线程),从上到下执行
* @param args
*/
public static void main(String[] args) {
System.out.println("我是Main方法,我是入口");
MyThread thread1 = new MyThread();// 线程一
thread1.setName("线程一");
MyThread thread2 = new MyThread();// 线程二
thread2.setName("线程二");
thread1.start(); // 开启线程,线程进入到就绪模式,准备抢CPU
//thread1.run();// 不要直接run
thread2.start();
//thread2.run();
System.out.println("Main方法结束.....");
}
}
给线程起名字:
1) 默认每个线程都有自己的默认的名字 Thread-xxxx
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
2) 修改线程的名字
一、直接调用setName方法修改
MyThread thread1 = new MyThread();// 线程一
thread1.setName("线程一");
二、创建线程的时候通过构造方法指定<br /> ** 复习:继承中,子类的构造方法必须先调用父类的构造方法,父类的构造有有参和无参。**
public Thread(String name) {
init(null, null, name, 0);
}
怎么调用Thread里面的有参数的构造方法呢?
public MyThread(String name){
//public Thread(String name) {
// init(null, null, name, 0);
// }
super(name);
}
MyThread thread3 = new MyThread("线程三");
thread3.start();
2、实现Runnable接口
package com.qfedu.day13;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/3/17 11:07
* @Version 1.0
*/
public class TestRunnable {
/**
* 创建多线程的第二种方式
* @param args
*/
public static void main(String[] args) {
System.out.println("main 开始.....");
MyRunnable myRunnable = new MyRunnable();
//myRunnable.run();
Thread thread1 = new Thread(myRunnable,"窗口1");
Thread thread2 = new Thread(myRunnable,"窗口2");
thread1.start();
thread2.start();
System.out.println("main 结束.....");
}
}
两种创建方式对比:
继承Thread 是方案一, 实现Runnable 接口是方案二
1、优先使用 方案二
2、 方案一是继承,java是单继承的,有局限性,而接口,一个类可以实现多个接口。
3、关于多线程的操作,都是Thread类,Thread类是专门管理多线程的,比如启动,设置名字等。而Runnable只是编写业务代码,将两者分开更加合理(代码的解耦)【高内聚低耦合】
package com.qfedu.day13;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/3/17 11:07
* @Version 1.0
*/
public class TestRunnable {
/**
* 创建多线程的第二种方式
* @param args
*/
public static void main(String[] args) {
System.out.println("main 开始.....");
MyRunnable myRunnable = new MyRunnable();
//myRunnable.run();
Thread thread1 = new Thread(myRunnable,"窗口1");
Thread thread2 = new Thread(myRunnable,"窗口2");
thread1.start();
thread2.start();
Runnable runnable = new Runnable() {
@Override
public void run() {
}
};
Thread thread3 = new Thread(runnable,"窗口3");
/**
* 以上代码也可以使用lambda 表达式来编写,但是不利于代码的重复利用,创建多个线程的话,run中的方法需要复制好几份。
*/
Thread thread4 = new Thread(() -> System.out.println("线程内容是...."), "窗口四");
System.out.println("main 结束.....");
}
}
三、多线程的线程安全问题
现象: 多个线程访问同一个资源,有可能出现线程不安全的问题,这种情况就是线程出现了安全问题。
窗口2买了第1张票
窗口4买了第1张票
窗口4买了第3张票
窗口4买了第4张票
窗口4买了第5张票
窗口4买了第6张票
窗口4买了第7张票
窗口1买了第1张票
窗口3买了第1张票
窗口1买了第9张票
窗口4买了第8张票
窗口4买了第12张票
窗口2买了第2张票
窗口4买了第13张票
窗口1买了第11张票
窗口3买了第10张票
窗口1买了第16张票
窗口4买了第15张票
窗口2买了第14张票
窗口4买了第19张票
窗口4买了第21张票
窗口1买了第18张票
窗口1买了第23张票
窗口1买了第24张票
窗口1买了第25张票
窗口1买了第26张票
窗口3买了第17张票
窗口3买了第28张票
窗口3买了第29张票
窗口3买了第30张票
窗口1买了第27张票
窗口1买了第32张票
窗口4买了第22张票
窗口2买了第20张票
窗口4买了第34张票
窗口4买了第36张票
窗口1买了第33张票
窗口3买了第31张票
窗口1买了第38张票
窗口1买了第40张票
窗口1买了第41张票
窗口4买了第37张票
窗口4买了第43张票
窗口4买了第44张票
窗口4买了第45张票
窗口2买了第35张票
窗口2买了第47张票
窗口2买了第48张票
窗口2买了第49张票
窗口2买了第50张票
窗口4买了第46张票
窗口1买了第42张票
窗口1买了第53张票
窗口1买了第54张票
窗口3买了第39张票
窗口1买了第55张票
窗口4买了第52张票
窗口2买了第51张票
窗口4买了第58张票
窗口1买了第57张票
窗口1买了第61张票
窗口3买了第56张票
窗口1买了第62张票
窗口4买了第60张票
窗口2买了第59张票
窗口2买了第66张票
窗口2买了第67张票
窗口4买了第65张票
窗口1买了第64张票
窗口3买了第63张票
窗口1买了第70张票
窗口4买了第69张票
窗口2买了第68张票
窗口4买了第73张票
窗口1买了第72张票
窗口3买了第71张票
窗口1买了第76张票
窗口4买了第75张票
窗口2买了第74张票
窗口4买了第79张票
窗口1买了第78张票
窗口1买了第82张票
窗口1买了第83张票
窗口3买了第77张票
窗口1买了第84张票
窗口4买了第81张票
窗口2买了第80张票
窗口4买了第87张票
窗口1买了第86张票
窗口3买了第85张票
窗口1买了第90张票
窗口4买了第89张票
窗口2买了第88张票
窗口4买了第93张票
窗口1买了第92张票
窗口3买了第91张票
窗口1买了第96张票
窗口4买了第95张票
窗口2买了第94张票
窗口4买了第99张票
窗口1买了第98张票
窗口3买了第97张票
窗口1买了第102张票
票已售罄
窗口2买了第100张票
窗口1买了第104张票
窗口3买了第103张票
Process finished with exit code 0
里面有卖重的,以及超卖的情况。
package com.qfedu.day13_02;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/3/17 11:27
* @Version 1.0
*/
public class Ticket implements Runnable{
static int ticketNum = 100;
boolean flag = true;
@Override
public void run() {
while(flag){
System.out.println(Thread.currentThread().getName()+"买了第"+(101-ticketNum)+"张票");
ticketNum-- ;
if(ticketNum == 0){
System.out.println("票已售罄");
flag = false;
}
}
}
}
解决多线程的方案:加锁(synchronized)
synchronized ("锁"){
System.out.println("xxxxxx");
System.out.println("xxxxxx");
System.out.println("xxxxxx");
}
可以使用一把锁,把需要锁住的代码(关键性代码)给锁住,就线程安全了。
关键性的代码: 关于同一资源的代码: ticketNum 就是同一资源。
什么样的东西可以当锁?
多个线程中的唯一性内容就可以当锁,锁必须是唯一 的。
package com.qfedu.day13_02;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/3/17 11:27
* @Version 1.0
*/
public class Ticket implements Runnable {
static int ticketNum = 100;
boolean flag = true;
static Object o = new Object();// 这个o 是唯一的,唯一的就而已当锁
@Override
public void run() {
while (flag) {
// synchronized (new Object()){ // 锁不住
// synchronized ("abc"){ // 能锁住 这个字符串,会被创建在常量池中,共享
synchronized (o) {
if (ticketNum == 0) {
System.out.println("票已售罄");
flag = false;
}else{
System.out.println(Thread.currentThread().getName() + "买了第" + (101 - ticketNum) + "张票");
ticketNum--;
}
}
}
}
}
synchronized 关键字的使用:
synchronized 除了可以锁代码块,还可以锁方法
synchronized 方法上,方法又分为两种: 普通方法还有静态方法
修饰普通方法:锁是什么?
public synchronized void run()
锁就是这个方法对应的类的实例化对象,比如Ticket ticket = new Ticket(); ticket 就是锁。
修饰静态方法,锁是什么?
public synchronized static void run()
这个锁就是该方法对应的类的字节码文件 Tikcket.class
选择问题:
什么时候锁方法,什么时候锁代码块?
优先使用代码块, 锁可以提供安全,但是效率低。
锁住的代码:每一时刻只能有一个线程使用,使用完再释放,线程一多,就阻塞了。
方法中的代码可能很多,只有一部分出现了线程安全问题,只需要考虑那一部分即可
三、常见集合的安全性
package com.qfedu.day13_03;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/3/17 15:54
* @Version 1.0
*/
public class CollectionDemo {
public static void main(String[] args) {
// 线程不安全
List<String> _list = new ArrayList<>();
// Vector() List集合中线程安全的 public synchronized boolean add(E e) {}
// Vector 是线程安全的,但是效率低
//List<String> list = new Vector<>();
// 底层还是拿 同步锁 解决线程不安全
//List<String> list = Collections.synchronizedList(_list);
// 安全并且效率高
List<String> list = new CopyOnWriteArrayList<>();
// new CopyOnWriteArraySet<>(); // set 集合对应的线程安全的类
// new ConcurrentHashMap<String,String>(); // hashMap对应的线程安全的集合类型
// UUID 就是一个随机字符串,36位,带4个- , 去掉- = 32 位
String uuid = UUID.randomUUID().toString();
System.out.println(uuid);
/**
* 我启动30个线程,同时向list中插入元素
* 插入的字符串是UUID的一部分
*/
for (int i = 0; i < 300; i++) {
new Thread(()->{
for (int j = 0; j < 10; j++) {
String _uuid = UUID.randomUUID().toString();
list.add(_uuid.substring(0,8));
}
},"线程"+(i+1)).start();
}
System.out.println(list);
}
}
四、多线程创建的第三种方式—Callable接口
package com.qfedu.day13_03;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/3/17 16:23
* @Version 1.0
*/
/**
* 一般可以用于子线程需要跑一个任务,这个任务工作量很大,并且需要返回值 可以考虑使用Callable
*/
class A implements Callable<String>{
@Override
public String call() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"++++"+i);
}
return "你好";
}
}
public class Thread3 {
public static void main(String[] args) throws Exception {
System.out.println("主线程.......");
// 未来的任务,可以将一些耗时的操作,交给他完成。
FutureTask<String> futureTask = new FutureTask<String>(new A());
//futureTask.get();
// FutureTask 为什么可以放入到Thread中,因为他是Runnable接口的实现类
new Thread(futureTask).start();
// 想获取子线程的返回值,需要通过get 方法,只有子线程执行完,才能拿到结果。
System.out.println(futureTask.get());
System.out.println("主线程结束了.......");
}
}
1) 使用Callable接口
和Runnable接口相比
1、有返回值
2、抛异常
3、需要放入FutureTask中才能使用