目录
一、什么是进程?
在了解多线程之前,我们得了解一下,什么是进程,进程简单理解就是,一个app,或者一个程序跑起来,就是进程,当然也有多进程,比如电脑qq,电脑微信,只要微信号或者qq号多,一般都可以同时挂在电脑上,这些都是多进程,相信大家电脑肯定会遇到,运行某个软件电脑死机的时候,这个时候我们会通常打开任务管理器ctrl+shift+esc,找到对应的应用,来结束他的进程,电脑就不卡了,结束进程就是关闭这个应用,打开应用相当于开启了这个进程。
二、什么是线程?
那好接下来说说什么是多线程?从java语言角度来说,多线程就是在一个Java程序中同时运行多个线程的能力,可以将多线程类比为一个车间,每个线程类比为车间内的一个工人。在单线程的情况下,只有一个工人在车间中工作,而在多线程的情况下,可以有多个工人同时在车间中工作,从而加快任务的完成速度。也可以理解为,我qq可以同时给多个人发信息,这个也是多线程。
三、什么是并发与并行?
并发是指一个系统能够同时处理多个任务或多个事件的能力,举个例子,厨师洗菜、切菜、炒菜、上桌,客人点了菜,厨师一个人在同一时间轮流交替的完成,这就叫并发。我们中国就有一个超牛的软件12306,他可以承受亿级别的并发量,做到千万人同时在线买票。在计算机领域,多个任务或多个事件可以同时被处理是通过交替执行和时间片轮转等技术实现的。并发能够提高系统的处理效率和资源利用率,提升用户的体验和系统的响应性。常见的并发技术包括多线程、多进程、协程等。
并行是指同时进行多个任务或操作的能力。举个例子,配菜人员准备食材、砧板工切菜、厨师炒菜、服务员上菜,专人专职,各施其职,互不干扰,这就叫并行。在计算机科学中,指的是多个计算任务同时进行的能力。并行可以通过将任务分成多个子任务,然后同时进行这些子任务来实现。通过并行计算,可以有效地提高计算机系统的处理能力和效率。
四、java实现多线程
1.继承java.lang.Thread类
public class TestThread1 extends Thread{
//重写Thread里面的run方法
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("我在吃饭-----"+i);
}
}
public static void main(String[] args) {
//实例化一条线程
TestThread1 testThread1=new TestThread1();
//多线程的启动方法
testThread1.start();
//main函数主线程
for (int i = 0; i < 20; i++) {
//获取线程名称
System.out.println("我在睡觉------"+i);
}
}
}
可以看到我有一个TestThread1类继承了Thread,并且重写了run方法,在maina函数主分支里面创建了一条线程,并且开启,运行我们发现,我在吃饭和我在睡觉在同时执行。至此多线程已经实现了。
2.实现接口java.lang.Runnable
public class TestThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("我在吃饭------"+i);
}
}
public static void main(String[] args) {
//创建Runable的实现类对象
TestThread2 testThread2=new TestThread2();
//创建线程对象
Thread thread=new Thread(testThread2);
//启动线程
thread.start();
//当然也可以向下面这样简写
new Thread(new TestThread2()).start();
//main函数主线程
for (int i = 0; i < 20; i++) {
System.out.println("我在睡觉------"+i);
}
}
}
运行也是一样的效果。
注意:线程必须通过start()方法启动。如果直接调用run()方法,只是方法调用,不是启动线程。
两种实现对比,实现Runnable接口有以下好处
1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码、数据有效的分离,较好地体现了面向对象的设计思想;
2)可以避免由于Java的单继承特性带来的局限;
3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码(run方法体)来自同一个类的实例(实现Runnable接口的对象)时,即称它们共享相同代码。
4)Thread
类实际上是Runnable
接口的实现类
五、常用API
1、start():启动线程
2、run():线程体
3、currentThread()获取当前正在运行的线程
4、getName():获取线程名称
5、setName(String):设置线程名称
6、getState():获取线程的状态
线程状态通过Enum表示,分别为:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
7、setDaemon(true):设置守护线程
setDaemon(true|false):线程默认为false
8、sleep():线程休眠
Thread.sleep(1000);
当前线程停止1000毫秒(1秒),1000毫秒过后继续执行
注意:
1.每个对象都有一个锁,sleep不会释放锁,相当于抱着锁睡觉
2.可以放大问题的发生性
9、join():合并线程
/**
* 测试join 可以想象为插队,合并线程
*/
public class TestJoin implements Runnable{
public static void main(String[] args) throws InterruptedException {
TestJoin testJoin=new TestJoin();
Thread thread=new Thread(testJoin);
//启动线程
thread.start();
for (int i=0;i<=500;i++){
if(i==10){
thread.join();//插队
}
System.out.println("main"+i);
}
}
@Override
public void run() {
for (int i=0;i<1000;i++){
System.out.println("vip"+i);
}
}
}
注意:在线程A中调用线程B的join()
方法,相当于让线程B直接插入线程A方法中运行(线程A阻塞),直至线程B结束,继续线程A,相当于插队
10.线程优先级
1)线程优先级:优先级越高,获取CPU的执行时间就越长
2)设置/获取优先级
- public final void setPriority(int newPriority)
- newPriority取值范围是1-10,值越大,优先级越高
- public final int getPriority()
3)优先级的三个常量
- public final static int MIN_PRIORITY = 1 ; //最低优先级
- public final static int NORM_PRIORITY = 5 ; //缺省优先级
- public final static int MAX_PRIORITY = 10 ; //最高优先级
注意:
- 高优先级并不意味着先执行,它只是提示任务调度器优先调度此线程,但任务调度器很有可能会忽略它。
- 只是原则上具有更高概率先抢占CPU资源执行。
- 如果CPU比较忙,则优先级高的线程会获得更多的时间片;如果CPU闲时,优先级几乎无作用。
设置优先级相当于自己骗自己。。。。
六:synchronized 线程同步
介绍:
synchronized是Java中的关键字,用于实现线程的同步。当一个方法或者一个代码块被synchronized修饰时,它表示在同一时间只能有一个线程可以执行该方法或者该代码块,其他线程需要等待。synchronized可以保证线程安全性,但它的性能比较低下,因为每个线程在执行synchronized代码时会进行锁的获取和释放操作。在Java 5以后,提供了更高效的锁机制,如ReentrantLock类和Lock接口,可以替代synchronized来实现线程同步
我们先来看一个列子:
List<Object> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(() ->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(200);
System.out.println("arrayList="+list.size());
这里是使用了lamda表达式,轻便快捷,按道理来说循环1000次线程名字,在存到集合里,我们在打印集合的数量应该是1000,但运行后发现不对劲,结果并不是1000,这里就是线程安全问题,记住list集合线程不安全,想要安全必须上锁,当然java也提供了线程安全的集合,比如CopyOnWriteArrayList,他里面实际上也是上了锁。
如何解决?我们先看API
方法一:同步语句块
synchronized (object) { ...具体代码逻辑 }
注意:object可以是任意的一个对象,但必须是唯一的;
方法二:同步方法
下面两种写法可以达到一样效果,那为什么会有这个区分呢?原因很简单,假如在多线程中你有100行代码,只有10行代码需要同步,其他不需,那就可以用语句块,不用同步所有代码,拉低性能。
[修饰符] synchronized 数据类型 方法名() { ...具体代码逻辑 }[修饰符] 数据类型 方法名([参数列表]) { synchronized(this) { ...具体代码逻辑 } }
相必现在我们应该知道如何保证list集合安全了
List<Object> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(() ->{
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(200);
System.out.println("arrayList="+list.size());
这里我们用了语句块,list只new一次所以唯一,这里就实现同步,size一直是1000了。
方法三:lock锁
lock锁和synchronized一样,也可以达到同步效果,但他们还是有一些区别,看下面一个列子
public class TestLock {
public static void main(String[] args) {
Lock lock=new Lock();
new Thread(lock).start();
new Thread(lock).start();
new Thread(lock).start();
}
}
class Lock implements Runnable{
//票数
private int num=100;
//定义lock锁
private final ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock();//加锁
Thread.sleep(100);
if (num <= 0) {
break;
}else{
//票数-1
System.out.println(num--);
}
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
finally {
lock.unlock();//解锁
}
}
}
}
这是一个简单的,3个人同时抢100张票的小案例,实现了线程同步,保证了安全。
区别:
1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,出了作用域自动释放 2.Lock只有代码块锁,synchronized有代码块锁和方法锁 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
七:线程通信
1.wait()
线程进入阻塞状态,释放锁(和sleep不同)
2.notify()
唤醒正在等待锁的线程,进入就绪状态(有优先级按优先级,没有随机唤醒一个);
3.notifyAll()
唤醒所有正在等待锁的线程;
说明:
- 这三种方法只能出现在同步代码块或同步方法里,且不能用在lock里,否则会报错
java.lang.IllegalMonitorStateException: current thread not owner
; - 同时,这三个方法是Object类定义的,也就是,所有对象都可以访问这三个方法,但是,一般通过当前的锁对象进行访问
- 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,默认情况下是this或者类.class(当前类的对象)
sleep()和wait()的异同
- 同
- 一旦使用,均可使当前线程进入阻塞状态;
- 异
- 调用要求不同:sleep()声明在Thread类中,wait()声明在Object类中;
- 声明位置不同:sleep()可以使用在各种需要的地方,而wait()只能用在同步代码块或同步方法里;
- 使用 sleep() 不会释放对象锁,而使用 wait() 会释放对象锁。
区别案例:
public class Test01 {
public static void main(String[] args) {
Object object=new Object();
Waiter waiter = new Waiter(object);
new Thread(waiter).start();
Notifier notifier = new Notifier(object);
new Thread(notifier).start();
System.out.println("所有线程已经启动");
}
}
class Waiter implements Runnable{
private Object object;
public Waiter(Object object) {
this.object=object;
}
public void run(){
synchronized (object) {
System.out.println("测试wait");
try {
object.wait();//会释放锁,sleep不会释放锁
System.out.println("wait后面输出的值");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class Notifier implements Runnable {
private Object object;
public Notifier(Object object) {
this.object=object;
}
public void run() {
try {
Thread.sleep(1000);
synchronized (object) {
System.out.println("测试notify");
// 如果有多个等待线程,也可以使用notifyAll()方法来唤醒所有等待线程
object.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出顺序是:
测试wait
所有线程已经启动
测试notify
wait后面输出的值
先输出前两句,间隔1000毫秒后,输出后面的值
object.wait();也可以设置参数,和sleep一样,不设置就是永久,直到object.notify();才能解开
八:线程死锁
介绍:
死锁是指在多线程编程中,两个或多个线程无限期地等待对方持有的资源,导致它们都无法继续执行的情况。通常发生在多个线程同时持有一些共享资源并试图获取对方持有的资源时。 当资源的获取顺序不当时,可能会发生死锁。
案例:
public class TestDeadLock {
private static Object resource1 = new Object();
private static Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: 占用资源1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1:等待资源2...");
synchronized (resource2) {
System.out.println("Thread 1: 有资源 1 和资源 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: 占用资源 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: 等待资源 1...");
synchronized (resource1) {
System.out.println("Thread 2: 拥有资源 2 和资源 1...");
}
}
});
thread1.start();
thread2.start();
}
}
重点:当一个线程获取了某个资源的锁(比如resource1),那么其他线程就无法获取该资源的锁,直到持有锁的线程释放了它 在案例代码中,Thread 1先尝试获取resource1的锁, 成功获取后,它会休眠一段时间(通过Thread.sleep(100)模拟)然后尝试获取resource2的锁。 同时,Thread 2先尝试获取resource2的锁,成功获取后,它也会休眠一段时间,然后尝试获取resource1的锁,由于是多线程执行,互相等待对方资源释放,这个时候就是死锁了。
如何避免死锁?
避免死锁是一种重要的并发编程技术,下面是一些常见的方法来避免死锁:
-
避免使用多个锁:减少锁的数量可以降低发生死锁的可能性。如果可能的话,可以考虑重构代码,减少对锁的依赖。
-
使用单一的锁:使用一个全局的锁来保护共享资源,而不是使用多个锁来保护不同的资源。这样可以避免死锁的发生。
-
使用固定的锁顺序:如果必须使用多个锁来保护多个资源,可以确保在获取锁的顺序上保持一致。这样可以避免死锁的发生。
-
设置锁超时:在获取锁的时候设置一个超时时间,如果超过指定时间还没有获取到锁,则放弃当前操作。这样可以避免死锁持续下去。
-
使用非阻塞的锁:使用非阻塞的锁机制,如自旋锁、读写锁等,可以避免线程因等待锁而阻塞,从而避免死锁的发生。
-
避免循环依赖:在设计并发程序时,应尽量避免出现循环依赖的情况,即一个线程等待另一个线程所持有的资源,而后者又在等待前者所持有的资源。
-
使用资源分配图进行检测:通过建立资源分配图,检测是否存在环路来判断是否可能发生死锁。如果存在环路,则说明可能会发生死锁,需要对程序进行优化或修改。
-
使用死锁检测工具:使用专门的死锁检测工具可以帮助发现并解决潜在的死锁问题。
请注意,死锁是一种复杂的并发问题,没有通用的解决方案。因此,在设计并发程序时,应该仔细考虑和评估各种可能的情况,并通过合理的设计和测试来尽量避免死锁的发生。
九:分享与心得
本文是自己学习,总结下来的笔记,在此进行分享,哪里写错了请指出,我会加以改正