一 进程和线程的概念
进程:
进程是正在运行的程序。确切来说,进程就是当程序进入到内存中运行的时候形成的,进程是出于运行中的程序,并且具有一定的独立功能。
线程:
线程是进程中的一个执行单元,负责当前进程中的程序的执行,一个进程中至少拥有一个线程,一个进程中可以拥有多个线程的,这个程序也可以称为多线程程序。
二 抢占式调度
抢占式调度指的是优先级高的线程抢占到cpu的资源的概率也就越大。如果优先级相同,那么会随机挑选一个线程进行执行,这种方式就成为抢占式调度,Java采用的就是这一种调度。
cpu使用抢占式调度模式是在多个线程之间进行高速的切换,对于cpu的一个核来说,某一个时刻只能执行一个线程。由于cpu在线程之间的切换速度非常快,所以对于人类肉眼来说,开上去就像是在同时运行一样。
总结:java采用的是抢占式调度,cpu的一个核在一个时刻只能执行一个线程,并不是真正意义上的多线程同时进行,而是在不同线程之间高速切换,造成多线程同时运行的假象,但如果是多核的cpu(现在的cpu大多数都是多核的),则就是真正意义上的多线程同时进行。
三 多线程的优点
3.1 提高用户体验
3.2 提高cpu利用效率
多线程程序并不能提高程序的运行速度,但是可以提高cpu的运行效率,使得cpu的利用率得到了很大的提升。因为计算机执行任务是在cpu上执行的,而线程在执行执行中会与计算机硬件进行交互,此时会暂时的空置cpu,在cpu空置的时候切换到其他线程并执行,这样就显著的提高的cpu的利用率。
例如一个线程对cpu的利用率是80%,那么两个线程对cpu的利用率就高达96%。(在空置的20%中又利用了80%
四 主线程的概念
在JVM启动后,必然有一个执行路径,即线程,从main方法开始执行,知道main方法结束以后。这个线程在Java中被称为主线程或者main线程。当程序在主线程中遇到了循环而导致程序在执行的位置的停留时间过长,则无法马上执行循环下方的程序,需要等待循环结束后才能继续执行下面的程序,解决办法就是我们可以去开辟一条新的线程去执行比较耗时的任务。
五 定义线程的方式
在Java中,线程的顶级父类是Thread。
5.1 用实现了Runnable接口的Thread方法来定义,得继承Thread类
创建步骤
1.定义一个类去继承Thread。
2.重写Thread中的run方法,把需要执行的代码放置在run方法中。
3.创建子类(继承了Thread的类)对象
4.通过子类对象调用start()方法。该方法会开启新的线程去执行run方法。
public class ThreadTestDemo1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"在执行"+i);
}
}
}
public class Main {
public static void main(String[] args) {
ThreadTestDemo1 threadTestDemo1 = new ThreadTestDemo1();
threadTestDemo1.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"在执行"+i);
}
}
}
5.2 直接实现Runnable接口来定义
1.定义类实现Runnable接口
2.重写Runnable接口中的run方法
3 将Runnable接口的子类对象作为参数传递给Thread的构造方法中
4调用Thread类的start方法开启线程
//创建Runnable接口的实现类
class ThreadTest1 implements Runnable{
//重写run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"在执行"+i);
}
}
}
public class Main {
public static void main(String[] args) {
//创建子类对象作为形参传入线程创建中
ThreadTest1 t1 = new ThreadTest1();
Thread thread1=new Thread(t1);
thread1.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"在执行"+i);
}
}
}
六 线程的安全隐患
如果有多个线程在同时运行,那么就有可能会有多个线程在同时运行一段代码,程序,此运行的结果如果和单线程的结果保持一致,而且其他变量的值也和预期的一样,那么就是线程安全,如果和预期的不一样,那么就是线程不安全。
总结:出现线程安全隐患的条件
1.多条线程
2.共享相同的资源
3.有修改操作(增删改查)
七 线程安全隐患的解决方法(synchornized)
解决线程安全隐患的方法就是破坏满足出现线程安全隐患的三个条件之一即可,使其无法满足出现线程安全隐患的条件。
7.1 方法一:同步代码块
同步代码块中能保证在{ }中仅有一个线程能够执行。该线程执行完之后其他线程再去执行
格式:
synchronized(锁对象){
}
同步代码块中的对象可以是任意的对象。但是当有多个线程的时候,要使用同一个锁对象才能保证线程安全。因为方法区的所有内容都是线程共享的,所以方法区的所有内容都可以看作为锁对象。
public class ThreadTest2 implements Runnable{
//票是多个线程共享的资源
static int ticket=100;
@Override
public void run() {
Object lock=new Object();
while (true){
try {
//让当前线程休眠1ms
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock) {
if(ticket<=0){
break;
}
System.out.println(Thread.currentThread().getName()+"您的座位是"+ticket--);
}
}
}
}
7.2 方法二:同步方法和静态同步方法
7.2.1 同步方法
同步方法能保证在方法中,同一时刻只有一条线程在执行。
格式: 同步方法的锁对象为 this
private synchronized void method1(){
//可能会产生线程安全的代码
}
7.2.2静态同步方法
同步方法能保证在方法中,同一时刻只有一条线程在执行。
格式:静态同步方法的锁对象为 类名.class
//静态同步方法 静态同步方法的锁对象为 类名.class
private synchronized static void method1(){
//可能会产生线程安全的代码
}
八 锁
8.1 synchornized
该锁为自动上锁和自动释放,具体用法在线程安全隐患解决部分以及讲过
8.2 lock
lock等同于synchornized 但是需要手动上锁和释放锁,后者则是自动上锁和释放锁,现在lock已经被synchornized取代。
上锁:lock.lock();
解锁:lock.unlock();
8.3 死锁
死锁的概念
死锁(Deadlock)是指多个线程在执行过程中,因为争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去。死锁通常发生在多个线程互相持有对方所需的资源,并且都不释放自己持有的资源,从而导致所有线程都无法继续执行。
死锁的四个必要条件
- 互斥条件:资源一次只能被一个线程占用。
- 占有并等待:线程持有至少一个资源,并且正在等待获取其他被占用的资源。
- 非抢占条件:线程持有的资源不能被其他线程强行抢占,必须由线程自己释放。
- 循环等待条件:存在一个线程等待的循环链,每个线程都在等待下一个线程所持有的资源。
Java 中的死锁示例
以下是一个简单的 Java 代码示例,展示了死锁的发生:
public class DeadlockExample {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Holding resource 1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for resource 2...");
synchronized (resource2) {
System.out.println("Thread 1: Holding resource 1 and resource 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Holding resource 2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for resource 1...");
synchronized (resource1) {
System.out.println("Thread 2: Holding resource 2 and resource 1...");
}
}
});
thread1.start();
thread2.start();
}
}
在这个例子中,thread1
持有 resource1
并等待 resource2
,而 thread2
持有 resource2
并等待 resource1
,从而导致死锁。
如何避免死锁
- 避免嵌套锁:尽量避免在一个线程中持有多个锁,或者确保所有线程以相同的顺序获取锁。
- 使用超时机制:在尝试获取锁时设置超时时间,如果超时则释放已持有的锁并重试。
- 使用工具检测死锁:使用工具如
jstack
或VisualVM
来检测和分析死锁。
检测死锁
在 Java 中,可以使用 jstack
工具来检测死锁。jstack
是 JDK 自带的一个命令行工具,可以生成 Java 虚拟机当前时刻的线程快照。通过分析线程快照,可以找到死锁的线程和资源。
jstack <pid>
其中 <pid>
是 Java 进程的进程 ID。运行该命令后,jstack
会输出线程的堆栈信息,如果存在死锁,会在输出中明确指出。
总结
死锁是多线程编程中常见的问题,理解其产生的原因和必要条件有助于编写更健壮的并发程序。通过合理的锁管理、超时机制和工具检测,可以有效避免和解决死锁问题。
九 同步与异步及线程安全与不安全的数据结构
9.1 同步和异步
同步:一段代码在同一时刻只允许一个线程执行
异步:一段代码在同一时刻允许多个线程执行
同步一定是线程安全,线程安全不一定同步。
异步不一定线程不安全,线程不安全也不一定异步。
9.2 线程安全的结构
Vector
Hashtable
ConcurrentHashMap
CopyOnWriteArrayList
CopyOnWriteArraySet
StringBuffer
BlockingQueue
及其实现类(如ArrayBlockingQueue
,LinkedBlockingQueue
)ConcurrentLinkedQueue
ConcurrentSkipListMap
ConcurrentSkipListSet
SynchronizedList
(通过Collections.synchronizedList
创建)SynchronizedSet
(通过Collections.synchronizedSet
创建)SynchronizedMap
(通过Collections.synchronizedMap
创建)
9.3 线程不安全的结构
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
PriorityQueue
StringBuilder
LinkedList
ArrayDeque
LinkedHashMap
WeakHashMap
IdentityHashMap
EnumMap
BitSet
这些结构在多线程环境下使用时,线程安全的结构可以直接使用,而线程不安全的结构需要通过同步机制(如 synchronized
关键字或使用 Collections.synchronizedXXX
方法)来保证线程安全。