一、概念理解
进程:一个软件只要运行了,就可以算是一个进程。
线程:同时对多个任务进行控制,比如,微信上可以发消息,语音通话,传文件。这些功能可以同时进行。
注解:其实对于单核的CPU也是支持多线程的,在接收到命令的时候会有多个线程抢占CUP的使用权,抢到就执行,执行完之后就释放资源,其他未执行的线程就在就绪队列中继续抢占。其实他们并不是同一时间进行的。
二、创建多线程的3种方式
1、继承Thread类
package com.day13;
/**
* @Author
* @Description TODO
* @Date 2022/3/17 10:12
* @Version 1.0
*/
public class MyThread extends Thread{
/**
* 每一个方法都有一个无参数的构造方法,如果是继承,会自动调用父类的无参构造
*/
public MyThread(){
super();
}
@Override
public void run(){
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"******");
}
}
}
package com.day13;
/**
* @Author
* @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();
MyThread thread2 = new MyThread();
thread1.start(); //开启线程,线程进入就绪模式,准备抢占CPU
//thread1.run();
thread1.setName("线程一");
thread2.start();
//thread2.run();
thread2.setName("线程二");
System.out.println("Main测试方法结束");
}
}
修改线程的名字:直接调用setName()方法修改。
MyThread thread1 = new MyThread();// 线程一
thread1.setName("线程一");
创建线程的时候通过构造方法指定
复习:继承中,子类的构造方法必须先调用父类的构造方法,父类的构造有有参和无参。
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接口
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、方案一是继承,java是单继承的,有局限性,而接口,一个类可以实现多个接口。所以优先使用 方案二。
2、关于多线程的操作,都是Thread类,Thread类是专门管理多线程的,比如启动,设置名字等。而Runnable只是编写业务代码,将两者分开更加合理(代码的解耦)【高内聚低耦合】
package com.day13;
/**
* @Author
* @Description TODO
* @Date 2022/3/17 11:05
* @Version 1.0
*/
public class MyRannable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"******"+i);
}
}
}
package com.day13;
/**
* @Author
* @Description TODO
* @Date 2022/3/17 11:07
* @Version 1.0
*/
public class TestRannable {
public static void main(String[] args) {
//创建多线程的第二种方式
MyRannable myRannable = new MyRannable();
Thread thread1 = new Thread(myRannable,"窗口一");
Thread thread2 = new Thread(myRannable,"窗口二");
thread1.start();
thread2.start();
Runnable runnable = new Runnable() {
@Override
public void run() {
}
};
Thread thread3 = new Thread(runnable,"窗口三");
/**
* 以上代码也可以使用lambda 表达式来编写,但是不利于代码的重复利用,创建多个线程的
*/ 话,run中的方法需要复制好几份。(lambda只能用一次)
Thread thread4 = new Thread(() -> System.out.println("线程内容是:"),"窗口四");
System.out.println("main结束......");
}
}
3、Callable接口
package com.day13;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* @Author
* @Description TODO
* @Date 2022/3/17 21:01
* @Version 1.0
*/
class Test 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 CallableTest {
public static void main(String[] args) throws Exception {
System.out.println("主线程.......");
// 未来的任务,可以将一些耗时的操作,交给他完成。
FutureTask<String> futureTask = new FutureTask<String>(new Test());
//futureTask.get();
// FutureTask 为什么可以放入到Thread中,因为他是Runnable接口的实现类
new Thread(futureTask).start();
// 想获取子线程的返回值,需要通过get 方法,只有子线程执行完,才能拿到结果。
System.out.println(futureTask.get());
System.out.println("主线程结束了.......");
}
}
Callable接口与Runnable接口对比:
1、有返回值
2、抛异常
3、需要放入FutureTask中才能使用
三、多线程的线程安全问题
当多个线程访问同一个资源,有可能出现线程不安全的问题,这种情况就是线程出现了安全问题。
临界资源:
在一个进程中, 多个线程之间是可以资源共享的。 如果在一个进程中的一个资源同时被多个线程访问, 这个资源就是一个临界资源。如果多个线程同时访问临界资源, 会对这个资源的值造成影响。
临界资源问题
多个线程同时访问一个资源的情况下, 一个线程在操作这个资源的时候, 将值取出进行运算, 在还没来得及进行修改这块空间的值之前, 值又被其他的线程取走了。 此时就会出现临界资源的问题, 造成这个资源的值出现不是我们预期的值。
解决方案
临界资源问题出现的原因就是多个线程在同时访问一个资源, 因此解决方案也很简单, 就是不让多个线程同时访问即可。在一个线程操作一个资源的时候, 对这个资源进行“上锁”, 被锁住的资源, 其他的线程无法访问。
经典案例:
分析: 在实现四个售票员售票的案例中,出现了一些不正常的问题(一共100张票却在买票的过程中超出了总数),影响了线程安全,在程序运行结束后出现了如下情况:
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;
}
}
}
}
package com.day13;
/**
* @Author
* @Description TODO
* @Date 2022/3/17 11:31
* @Version 1.0
*/
public class MainTest {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread w1 = new Thread(ticket,"窗口1");
Thread w2 = new Thread(ticket,"窗口2");
Thread w3 = new Thread(ticket,"窗口3");
Thread w4 = new Thread(ticket,"窗口4");
w1.start();
w2.start();
w3.start();
w4.start();
}
}
运行结果:
解决多线程的方案:加锁(synchronized)
可以使用一把锁,把需要锁住的代码(关键性代码)给锁住,就线程安全了。
关键性的代码: 关于同一资源的代码:以上案例中 ticketNum 就是同一资源。
什么样的东西可以当锁?多个线程中的唯一性内容就可以当锁,锁必须是唯一的(也就是li资源)。
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
synchronized问题:
什么时候锁方法,什么时候锁代码块?
优先使用代码块, 锁可以提供安全,但是效率低。
锁住的代码:每一时刻只能有一个线程使用,使用完再释放,线程一多,就阻塞了。
方法中的代码可能很多,只有一部分出现了线程安全问题,只需要考虑那一部分即可。
三、常用集合的安全性
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);
}
}