多线程基础知识点
java多线程并发相关的基础小知识
概念
并发与并行
- 并发:多个事件在同一时间段内发生;
- 并行:多个事件在同一时间发生(同时发生,一起执行);
线程与进程
进程
指内存中运行的一个程序,每个进程都有一个独立的内存空间;
一个应用程序可以同时运行多个进程;
进程是程序的一次执行过程,是系统运行程序的一个基本单位;
系统运行一个程序即一个进程从创建、运行到消亡的过程;
线程
线程是进程的一个执行单元,负责当前进程中程序的执行;
一个进程至少有一个线程;
一个进程可以有多个线程;
线程的调度:
分时调度:所有线程轮流使用CPU,平均分配每个线程占用CPU的时间;
抢占式调度:优先级高的线程优先使用CPU;
创建线程的方法
继承Thread类
public class Demo1 {
public static void main(String[] args) {
MyThread1 myThread = new MyThread1();
myThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main thread: " + i);
}
}
}
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("new thread: " + i);
}
}
}
实现Runnable接口
public class Demo2 {
public static void main(String[] args) {
MyThread2 myThread2 = new MyThread2();
Thread thread = new Thread(myThread2);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main thread: " + i);
}
}
}
class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("new thread: " + i);
}
}
}
Thread与Runnable的区别
实现Runnable接口比继承Thread类的优势:
适合多个相同程序代码的线程去共享一个资源(如抢票功能的票数):
Runnable比Thread共享一个资源要方便一些(如下段代码所示);
可以避免java中的单继承的局限性:
java是单继承多实现的,继承了一个类后,就不能再继承其他类,有较大的局限性;
实现解耦操作,代码可以被多个线程共享,代码和线程独立:
一段代码可以被多个线程同时使用,每新启动一个线程,都会在JVM里新开一个栈空间,但是每个栈的执行体,所执行的都是同一段代码;
暂时抛开线程安全问题执行下面两段代码后,会发现继承Thread的线程所操作的ticket票数属性,是每个售票口10张票,开的两个线程之间资源没有共享(如果给ticket加上static,也是可以共享的),而实现Runnable的线程,则是所有售票口共同销售10张票;
Thread:
public class Demo3 {
public static void main(String[] args) {
MyThread3 thread1 = new MyThread3("售票口1");
thread1.start();
MyThread3 thread2 = new MyThread3("售票口2");
thread2.start();
}
}
class MyThread3 extends Thread {
private Integer ticket = 10;
public MyThread3(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}
}
}
Runnable:
public class Demo4 {
public static void main(String[] args) {
MyThread4 runnable = new MyThread4();
Thread thread1 = new Thread(runnable, "售票口1");
thread1.start();
Thread thread2 = new Thread(runnable, "售票口2");
thread2.start();
}
}
class MyThread4 implements Runnable {
private Integer ticket = 10;
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}
}
}
线程的常用方法
方法名 | 说明 |
---|---|
void setPriority(int) | 线程优先级为1-10,默认5,值越大,获取CPU机会越高 |
static void sleep(long millis) | 当前线程主动休眠millis毫秒 |
static void yield() | 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片 |
final void join() | 允许其他线程加入到当前线程中 |
void setDaemon(boolean) | 设置为守护线程有两类:false用户线程(前台线程),true守护线程(后台线程),默认为用户线程 |
设置优先级(setPriority)
调整java线程的优先级后,优先级高的线程会获得较多的运行机会,只能反应线程的紧急程度,不表示优先级高的就一定先执行;优先级为1到10,默认为5,值越大,获取CPU机会越高;
通过setPriority(int)方法来设置线程的优先级别,代码如下:
public class Demo5 {
public static void main(String[] args) {
MyThread4 runnable = new MyThread4();
Thread thread1 = new Thread(runnable, "thread1");
thread1.setPriority(1);
Thread thread2 = new Thread(runnable, "thread2");
thread2.setPriority(10);
thread1.start();
thread2.start();
}
}
休眠(sleep)
使用线程的sleep(long)可以使线程休眠指定的毫秒数,然后再继续执行执行线程;
代码示例:
当线程里for循环的i循环到5时休眠5秒。
public class SleepThread {
public static void main(String[] args) {
MyThread6 thread6 = new MyThread6();
thread6.start();
}
}
class MyThread6 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i == 5) {
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(i);
}
}
}
设置让步(yield)
Thread.yield()方法的作用是暂停当前正在执行的线程对象,放弃当前拥有的CPU资源,并执行其他线程,暂停的线程回到可运行状态,参与下一次的CPU资源竞争;
实际中无法保证yield()达到让步的目的,因为让步的线程可能在下一次竞争中再次被线程调度程序选中;
sleep()和yield()的区别:
sleep()是使当前的线程进入停滞状态,所以在指定时间内线程是肯定不会再继续执行的;
yield()是使当前线程回到可执行状态,进行下一次的资源竞争,是有可能在进入可执行状态后又获得了CPU的占有权,从而又被执行;
合并(join)
线程调用join()方法后,主线程的执行会被打断,知道加入的线程被执行完,主线程才会继续执行;
什么时候用join()方法
主线程启动了一个子线程,如果子线程的执行时间比较长,且主线程需要子线程的处理结果,也就是主线程需要等子线程执行完毕之后,主线程才能结束,这个时候就可以使用join()方法。
public class JoinThread {
public static void main(String[] args) throws InterruptedException {
System.out.println("----------main thread start----------");
Thread thread = new Thread(new MyThread7());
thread.start();
thread.join();
System.out.println("----------main thread end----------");
}
}
class MyThread7 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("child thread: " + i);
}
}
}
守护线程(setDaemon(true))
设置为守护线程之后,如果主线程执行完毕,则不管守护线程有没有执行完,都会立马结束;
示例代码如下:
MyThread8守护线程里的1000次循环还没执行完,就会随着主线程10次循环的执行完毕而结束;
public class DaemonThread {
public static void main(String[] args) {
MyThread8 thread = new MyThread8();
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main thread: " + i);
}
}
}
class MyThread8 extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("daemon thread: " + i);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程的生命周期
当线程被创建,并启动之后,它并不是立刻就进入了执行状态,也不是一直处于执行状态,而是进入了就绪状态(Runnable),等待CPU调度执行;
线程有5种状态:New、Runnable、Running、Blocked、Dead
如下图:
新建状态(New)
当线程对象创建后,即进入了新建状态,如:
Thread thread = new MyThread();
就绪状态(Runnable)
当调用了线程的 start() 方法之后,线程即进入了就绪状态,并不是说执行了thread.start() ,thread线程就会立即执行,而是说明该线程做好了准备,随时等待CPU的调度执行;
运行状态(Running)
当CPU调度处于就绪状态的线程时,该线程才会真正执行,即进入到运行状态;
线程想进入运行状态必须先进入就绪状态,就绪状态时进入运行状态的唯一入口;
阻塞状态(Blocked)
处于运行状态中的线程由于某一原因,暂时放弃了对CPU的使用权,停止执行任务,就会进入到阻塞状态,直到该线程重新进入到就绪状态,才会有机会被CPU重新调用,从而再次进入运行状态;
阻塞状态可分为3种:
等待阻塞:运行状态的线程执行wait()方法,使本线程进入到等待阻塞状态;
同步阻塞:线程在获取synchronized同步锁失败(锁被其他线程占用),会进入到同步阻塞状态;
其他阻塞:通过调用sleep()或join()或I/O请求时,线程会进入阻塞状态。当sleep状态超时,join等待线程终止或超时,或IO处理完毕,线程会重新进入就绪状态;
死亡状态(Dead)
线程执行完成,或因为异常退出了run()方法,该线程就结束了生命周期。
线程通信
等待唤醒机制
保证多个线程协同有序的一起完成一件事情;
当A线程需要等待B线程完成一个任务,才会继续接着执行后面任务时,使用Object.wait()方法,让A线程进入等待,然后等B线程执行完这个任务后,使用Object.notify()方法,通知A线程,将A线程从WAITING状态中唤醒,继续执行A线程后面的任务;
方法 | 说明 |
---|---|
public final void wait() | 释放锁,进入等待队列 |
public final void wait(long timeout) | 释放锁,进入等待序列,但超过设定时间之后,会自动唤醒 |
public final void notify() | 随机唤醒,通知所有线程 |
public final void notifyAll() | 唤醒、通知所有线程 |
场景案例
现有三个线程,一个老板,一个顾客,一起完成一个商品的交易;
交易过程如下(顺序不能乱):
1.顾客挑选货物,耗时2秒
2.顾客付钱
3.老板计算需要找零多少,耗时2秒
4.老板找零
5.顾客拿回零钱走人
代码示例
public class Demo6 {
public static void main(String[] args) {
// 锁
Object lock1 = new Object(); // 顾客付钱通知老板的锁
Object lock2 = new Object(); // 老板找零通知顾客的锁
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "==>1. 挑选货物");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "==>2. 付钱");
synchronized (lock1) {
lock1.notify();
}
synchronized (lock2) {
try {
lock2.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + "==>5. 拿回零钱走人");
}
}, "顾客").start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock1) {
try {
lock1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + "==>3. 计算需要找零多少");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "==>4. 找零");
synchronized (lock2) {
// 通知顾客找零了,可以走人了
lock2.notify();
}
}
}, "老板").start();
}
}
睡眠(sleep)和等待(wait)的区别
wait:线程不再活动,不再参与调度,进入到wait set(锁池)中,会释放锁,也不会去竞争锁,因此不会浪费cpu的资源,此时线程的状态为WAITING,需要等其他线程执行“通知(notify)”,才能将在这个对象上等待的线程从wait set中释放出来,重新进入调度队列(ready queue);
sleep:不会释放锁,会一直占用cpu资源;
wait与notify是Object类的方法,所以任意对象都有这两个方法;
死锁
两个或多个线程同时被阻塞,都在等待某一个锁资源的释放,由于线程被无限期的阻塞,所以程序不可能正常终止;
如两个线程A和B,两个锁资源lock1和lock2,线程A已经持有lock1,线程B已经持有lock2,但是现在线程A需要获取lock2才能继续往下执行,而线程B需要获取lock1才能继续往下执行,所以两个线程都需要对方已经持有的锁资源,此时两个线程就会因为互相等待对方释放资源,而进入到死锁的状态;
代码示例:
public class Demo7 {
public static void main(String[] args) {
Object lock1 = new Object(); // 锁资源1
Object lock2 = new Object(); // 锁资源2
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + "==>获取到锁1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "==>等待获取锁2");
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + "==>获取到锁2");
}
}
}
}, "线程A").start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + "==>获取到锁2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "==>等待获取锁1");
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + "==>获取到锁1");
}
}
}
}, "线程B").start();
}
}
线程池
线程池的顶级接口是java.util.concirrent.Executor,但Executor只是一个线程的执行工具,真正的线程池接口是java.util.concirrent.ExecutorService。
Executor类中创建线程池的方法如下:
newFixedThreadPool
newFixedThreadPool创建一个固定长度的线程池,当达到线程池的最大数量时,线程池的规模不会变化,多余的线程会等第一轮执行完后,第二轮去执行(不需要等待第一轮线程全部执行完,只要第一轮执行的某一线程执行完毕,可立即去执行第二轮)
代码示例:
class MyThread9 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": 煎鸡蛋");
try {
if (!"pool-1-thread-1".equals(Thread.currentThread().getName())) {
Thread.sleep(3000);
} else {
Thread.sleep(1000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ": 煎好鸡蛋");
}
}
public class Demo8 {
public static void main(String[] args) {
fixedThreadPool();
// cachedThreadPool();
// scheduledThreadPool();
// singleThreadPool();
}
/**
* 固定线程池数
*/
public static void fixedThreadPool() {
// 1. 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
// 2. 创建任务对象
MyThread9 thread = new MyThread9();
// 3. 提交任务对象
pool.submit(thread);
pool.submit(thread);
pool.submit(thread);
pool.submit(thread);
// 4. 关闭线程池(一般不做关闭)
pool.shutdown();
// 判断线程池是否被终止
while (!pool.isTerminated()) {
}
System.out.println("结束");
}
}
newCachedThreaPool
newCachedThreaPool创建一个可缓存的线程池,如果当前的线程池规模超出了处理的需求,将回收空的线程;当处理需求增加时,会扩大线程池规模,增加线程数量;线程池规模无限制;
/**
* 缓存线程池
*/
public static void cachedThreadPool(){
ExecutorService pool = Executors.newCachedThreadPool();
MyThread9 thread = new MyThread9();
pool.submit(thread);
pool.submit(thread);
pool.submit(thread);
pool.submit(thread);
pool.submit(thread);
pool.submit(thread);
}
newSingleThreadPoolExecutor
newSingleThreadPoolExecutor创建一个单线程的Executor,任务串行执行;
/**
* 单线程池
*/
public static void singleThreadPool(){
ExecutorService pool = Executors.newSingleThreadExecutor();
MyThread9 thread = new MyThread9();
pool.submit(thread);
pool.submit(thread);
pool.submit(thread);
pool.submit(thread);
pool.submit(thread);
}
newScheduledThreadPool
newScheduledThreadPool创建一个固定长度的线程池,而且以延迟或定时的方式来执行,类似Timer;
/**
* 延时/定时调度线程池
*/
public static void scheduledThreadPool(){
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
MyThread9 thread = new MyThread9();
pool.schedule(thread, 3, TimeUnit.SECONDS);
pool.schedule(thread, 3, TimeUnit.SECONDS);
pool.schedule(thread, 3, TimeUnit.SECONDS);
pool.schedule(thread, 3, TimeUnit.SECONDS);
pool.schedule(thread, 3, TimeUnit.SECONDS);
}
Callable接口
通过实现Callable接口实现的线程执行完毕会有返回结果,而使用Runnable接口和Thread实现的线程是没有返回结果的;
Callable只能在ExcutorService线程池中执行,可以通过返回的Future对象查询执行状态;
代码示例:
public class Demo9 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(3);
MyCallable callable = new MyCallable();
Future<Integer> result1 = pool.submit(callable);
Future<Integer> result2 = pool.submit(callable);
Future<Integer> result3 = pool.submit(callable);
Future<Integer> result4 = pool.submit(callable);
pool.shutdown();
while (!pool.isTerminated()) {
}
System.out.println("thread1: " + result1.get());
System.out.println("thread2: " + result2.get());
System.out.println("thread3: " + result3.get());
System.out.println("thread4: " + result4.get());
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
if ("pool-1-thread-1".equals(Thread.currentThread().getName())) {
Thread.sleep(2000);
}
Integer num = ((int)(Math.random() * 100));
System.out.println(Thread.currentThread().getName() + ": " + num);
return num;
}
}
线程安全集合
CopyOnWriteArrayList
是线程安全的ArrayList,读写分离;
写有锁,读无锁,读写之间互不阻塞,优于读写锁;
写入时先copy一个容器副本、再将根据该副本容器的数据复制到一个比该副本容器空间大1的新容器里,再将新元素添加到新容器的最后一个,最后将引用换为新容器的内存空间地址;
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
使用示例:
public class Demo10 {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10; j++) {
list.add((int) (Math.random() * 100));
}
}
});
}
pool.shutdown();
while (!pool.isTerminated()) {
}
// 应该有100条数(如果使用ArrayList,可能会有数据被覆盖,导致最后达不到100条数据)
System.out.println(list.size());
}
}
CopyOnWriteArraySet
对应HashSet的线程安全,使用方式与HashSet一样;
public class Demo11 {
public static void main(String[] args) {
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
set.add("张三");
set.add("李四");
set.add("李四");
System.out.println(set.size());
}
}
ConcurrentHashMap
- 对应HashMap的线程安全,使用方式同HashMap一样;
- 初始容量默认为16段(Segment),使用的是分段锁设计;
- 不对整个Map加锁,而是为每个Segment加锁;
- 当多个对象存入同一个Segment时,才需要互斥;
- 最理想状态为16个对象分别存入16个Segment,并行数量为16;
- HashTable不是用分段锁实现的,效率相对于ConcurrentHashMap较低;
代码示例:
在多线程中使用HashMap会提示ConcurrentModificationException异常
public class Demo12 {
public static void main(String[] args) {
// hashMap();
concurrentHashMap();
}
public static void hashMap(){
Map<String, String> map = new HashMap();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10; j++) {
map.put(Thread.currentThread().getName() + "---" + j, String.valueOf(j));
System.out.println(map);
}
}
}).start();
}
}
public static void concurrentHashMap(){
ConcurrentHashMap<String, String> map = new ConcurrentHashMap();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10; j++) {
map.put(Thread.currentThread().getName() + "---" + j, String.valueOf(j));
System.out.println(map);
}
}
}).start();
}
}