进程与线程
1.进程
进程共享堆不共享栈
2.线程
一个进程最少有一个线程,线程共享堆资源
并发与并行
1.并发Concurrent
2.并行parallel
同步与异步
1.同步
按照顺序一步步执行代码
2.异步Future
代码获取结果需要分开执行
那因此为什么提出多线程的概念?
提高CPU的利用率,加快任务执行效率。
不管进程还是线程,都可以理解为运行在CPU上的任务,不同的是进程运行的是一整个任务,而线程运行的是进程拆解后的小任务
Java中如何创建线程呢?
1.三种方式
继承Thread类
package com.lu.day12;
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {//多个线程对少量数据来说会降低效率
MyThread t1 = new MyThread(i);
//启动线程之后,run方法里面由操作系统执行;
t1.start();
//t1.run();
}
}
}
package com.lu.day12;
public class MyThread extends Thread{
private int count;
public MyThread(int count){
this.count = count;
}
@Override
public void run() {
System.out.println("run方法执行了"+count);
}
}
实现Runnable接口
package com.lu.day12;
import java.util.Arrays;
public class TestRunnable {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
java.lang.Runnable r1 = ()->{
int[] arr = {1,2,3,4,5};
int max = Arrays.stream(arr).max().getAsInt();
System.out.println("max:"+max);
};
Thread t1 = new Thread(r1);
t1.start();
}
}
package com.lu.day12;
import java.util.Arrays;
public class MyRunnable implements java.lang.Runnable {
@Override
public void run() {
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8,};
int sum = Arrays.stream(arr).sum();
System.out.println("sum:"+sum);
}
}
实现callable接口
package com.lu.day12;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TestCallable {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}
package com.lu.day12;
import java.util.Arrays;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8,};
return Arrays.stream(arr).sum();
}
}
2.练习多线程求和
用stream流的方式创建多线程求和
package com.lu.day12.sum;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.LongStream;
public class SumThread {
public static void main(String[] args) {
//并行流就是多线程计算
//数组的并行流,stream流可以开启并行流进行多线程计算parallel()
long[] array = LongStream.range(1, 1000000000).toArray();
System.out.println(Arrays.stream(array).parallel().sum());
//集合的并行流
ArrayList<Object> objects = new ArrayList<>();
objects.parallelStream();
}
}
3.三种方式的对比
Thread类中的一些方法
1.常见静态方法
package com.lu.day12;
public class Test2 {
public static void main(String[] args) {
try {
Thread.sleep(2000);
System.out.println("我是主线程");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//main主线程先执行剩下两个线程并行执行
Runnable r = ()->{
try {
Thread.sleep(2000);
System.out.println("hello");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
new Thread(r).start();
new Thread(()->{
try {
Thread.sleep(2000);
System.out.println("world");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
2.获取线程名称
3.设置线程名称
线程优先级
建议优先级,线程由操作系统统一管理(操作系统可以不接受建议)
守护线程
守护线程是一种特殊的线程,当进程中不存在用户线程了(用户线程run/call方法执行完毕了),则守护线程自动销毁。
典型的守护线程是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。
setDaemon
package com.lu.day12;
public class Test3 {
public static void main(String[] args) {
new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
Thread thread = new Thread(() -> {
while (true) {
System.out.println(1);
}
});
//设置守护线程
thread.setDaemon(true);
thread.start();
}
}
线程操作共享数据时的安全问题(重要)
package com.lu.day12.exercise;
import java.math.BigDecimal;
public class Account {
/**
* synchronized同步锁
* 1.为什么要给一个方法加锁?因为该方法在run/call中被多线程给调用了,
* 操作是同一个数据(变量)->对变量进行变化是一个非原子性操作。
* 2.加到什么地方?加到被run/call调用的方法上。
* 3.加锁的步骤:
* 1.获取锁(抢锁)
* 2.原子性的执行方法,直到执行完毕
* 3.释放锁(释放锁)
* 4.锁特点:
* 1.非公平锁->抢到锁执行释放锁之后可以继续参与枪锁
* 2.互斥锁->线程抢到锁之后,加锁的方法只能由该线程执行直到释放
* 3.独占锁->一把锁只能一个线程独自拥有,直到释放
* 4.隐式锁->加锁的步骤自己无法控制,看不见(全是由操作系统自己完成)
*
*/
private BigDecimal balance = BigDecimal.ZERO;
/**
* 存钱
*/
public synchronized void deposit(BigDecimal amount) {
balance = balance.add(amount);
}
public BigDecimal getBalance() {
return balance;
}
}
package com.lu.day12.exercise;
import java.math.BigDecimal;
public class Test {
public static void main(String[] args) throws InterruptedException {
Account account = new Account();
new Thread(()->{
for (int i = 0; i < 5; i++) {
account.deposit(new BigDecimal(100));
}
}).start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
account.deposit(new BigDecimal(100));
}
}).start();
Thread.sleep(2000);
System.out.println(account.getBalance());
}
}
//同步代码块,自己写一个锁,该锁一般都是一个object对象
synchronized (LOCK) {
balance = balance.add(amount);
}
1.JUC并法包 (面试题)
/* JUC包中线程安全的集合 单列 ArrayList/LinkedList -> CopyOnWriteArrayList(增删改加锁,查看加锁太浪费资源) -> 加同步锁 HashSet/TreeSet -> CopyOnWriteArraySet -> 加同步锁 双列 HashMap/TreeMap/LinkedHashMap -> ConcurrentHashMap -> 加同步锁 JUC下的线程安全类都有哪些 基本数据类型:四类八种 * 包装类:八种(浮点数无法精确计算,超过long,double的数无法存存储) * 大类:以Big开头的类BigDecimal、BigInteger * 以上都还有一个统一的缺点:多线程下无法保证原子性 * 原子类:以Atomic开头的类,保证基本数据类型的原子操作的原子类 非JUC包下的线程安全集合(如果使用优先使用JCU包下的) 单列 - > vector -> 增删改查 双列 - > Hashtable -> 增删改查全加锁 Collections.synchronized开头的方法也是用来线程安全的集合的 */
2.线程共享堆不共享栈
package com.lu.day12;
public class Testimony {
public static void main(String[] args) {
//多线程共享堆不共享栈
new Thread(() -> {
System.out.println(1 / 0);
}).start();
new Thread(() -> {
System.out.println(1 / 0);
}).start();
new Thread(() -> {
System.out.println(1 / 0);
}).start();
}
}
C:\Users\L\.jdks\corretto-17.0.11\bin\java.exe "-javaagent:D:\IntelliJ IDEA 2022.3.1\lib\idea_rt.jar=58188:D:\IntelliJ IDEA 2022.3.1\bin" -Dfile.encoding=UTF-8 -classpath D:\代码\advance84\out\production\advance84 com.lu.day12.Testimony
Exception in thread "Thread-1" Exception in thread "Thread-0" Exception in thread "Thread-2" java.lang.ArithmeticException: / by zero
at com.lu.day12.Testimony.lambda$main$0(Testimony.java:6)
at java.base/java.lang.Thread.run(Thread.java:840)
java.lang.ArithmeticException: / by zero
at com.lu.day12.Testimony.lambda$main$1(Testimony.java:9)
at java.base/java.lang.Thread.run(Thread.java:840)
java.lang.ArithmeticException: / by zero
at com.lu.day12.Testimony.lambda$main$2(Testimony.java:12)
at java.base/java.lang.Thread.run(Thread.java:840)
进程已结束,退出代码为 0
显示锁
private BigDecimal balance = BigDecimal.ZERO;
private final Lock lock = new ReentrantLock();
/**
* 存钱
*/
private final Object LOCK = new Object();
public void deposit(BigDecimal amount) {
lock.lock();
try {
balance = balance.add(amount);
} finally {
lock.unlock();
}
}
public BigDecimal getBalance() {
return balance;
}
死锁
package com.lu.day12;
public class Deadlock {
private static final Object LOCK1 = new Object();
private static final Object LOCK2 = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (LOCK1){
System.out.println("线程a抢到了LOCK1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (LOCK2){
System.out.println("线程a抢到了LOCK2");
}
}
}).start();
new Thread(()->{
synchronized (LOCK2){
System.out.println("线程b抢到了LOCK2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (LOCK1){
System.out.println("线程b抢到了LOCK1");
}
}
}).start();
}
}
C:\Users\L\.jdks\corretto-17.0.11\bin\java.exe "-javaagent:D:\IntelliJ IDEA 2022.3.1\lib\idea_rt.jar=61028:D:\IntelliJ IDEA 2022.3.1\bin" -Dfile.encoding=UTF-8 -classpath D:\代码\advance84\out\production\advance84 com.lu.day12.Deadlock
线程a抢到了LOCK1
线程b抢到了LOCK2
面试题
1.调用start方法启动线程与run方法的区别?
- 调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run() 方法;直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。
- 一个线程对线的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制。
2.多次调用start方法?
一个线程对线的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制。
3.创建多个线程观察运行顺序
无序的
4.java创建线程有几种方式,分别说一下哪几种
三种,第一种继承Thread类,第二种是实现实现Runnable接口,第三种是实现callable接口
5.你刚才讲的这几种线程的创建方式有什么不同?
继承Thread
类是最直接的方法。你只需要继承Thread
类并重写它的run
方法,然后调用start
方法启动线程。
优点:
- 简单直接,适合快速创建线程。
缺点:
- Java不支持多继承,因此如果你的类已经继承了另一个类,那么就不能再继承
Thread
类。 - 与实现
Runnable
接口相比,灵活性较差。
实现Runnable
接口是更灵活的一种方式。你只需实现Runnable
接口的run
方法,然后将该Runnable
对象传递给Thread
对象的构造方法。
优点:
- 更加灵活,因为你的类可以继承其他类。
- 代码更清晰,符合面向对象设计原则(组合优于继承)。
缺点:
- 不能直接返回结果。
实现Callable
接口是创建线程的第三种方式。与Runnable
接口不同,Callable
接口可以返回一个结果,并且可以抛出异常。
优点:
- 可以返回结果,通过
Future
对象获取。 - 可以抛出异常,便于异常处理。
缺点:
- 相对复杂一些,需要配合
ExecutorService
来使用。
6.线程操作共享数据时的安全问题
多线程在操作数据时会出现安全问题(非原子操作)操作系统给每一个线程固定的运行时间超过就会停止这个线程执行下一个线程,解决办法在方法上添加synchronized锁。