1.什么是线程
在了解线程之前,先了解一下进程。
进程:一段正在运行的程序。
线程:线程是进程中最小的调度的单元(单位),cpu控制的最小的执行单元。轻量级的进程。任何一个程序都至少有一个线程在用,多个线程共享内存。多线程切换消耗的资源少。
2.并发与并行
并发:在同一时间间隔内,同时有多个线程运行。(一个CPU调度多个线程)
并行:在同一时刻,同时有多个线程运行。(多个CPU调度多个线程)
3.线程和进程的关系?
线程是进程中最小的调度单元,线程必须存在于进程中。
进程是有自己的独立的内存空间。
多线程提高开发效率,多线程共享进程中的内存空间。
线程是由CPU进行控制调度,线程执行需要抢到CPU的时间片。
线程可以并发执行可以并行执行。
4.如何创建一个线程?
1.继承Thread的类,并且重写run方法,创建线程对象,调用.start()启动线程
public class MyThread1 extends Thread {public static void main(String[] args) { MyThread1 myThread1 = new MyThread1(); myThread1.start(); System.out.println("主线程正在进行"); }@Override public void run() { for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + "要唱歌" + i); } } }
run()和start()有什么区别?
run()在启动线程是会被自动调用,如果使用对象.run()则只是单单的调用run方法,start()则是启动线程的入口。
2.实现Runable接口,重写run()方法
public class MyRun implements Runnable {
public static void main(String[] args) {
MyRun myRun = new MyRun();
new Thread(myRun).start();
System.out.println("主线程正在进行");
myThread2.start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在思考" + i);
}
}
}
继承Thread和实现Runable接口有什么区别?
1、继承Thread:简单,受限于java单继承;
2、实现Runnable接口:继承类,实现多个接口,实现资源的共享,
3.实现Callable接口
这种方式可以获取线程的返回值。
public class MyCall1 implements Callable<String> {
public static void main(String[] args) throws ExecutionException, InterruptedException{
MyCall1 myCall1 = new MyCall1();
FutureTask<String> futureTask = new FutureTask<>(myCall1);
Thread thread = new Thread(futureTask);
thread.start();
String s = futureTask.get();
System.out.println(s);
}
@Override
public String call() throws Exception {
return "今天星期三";
}
}
总结
1、创建线程的三种方式:
-
继承Thread类
-
实现Runnable接口
-
实现Callable接口,配套使用FutureTask进行转换
2、什么时候用哪种方式?
-
如果有返回结果:Callable
-
如果没有返回结果:继承Thread类|实现Runnable接口
-
继承只能单继承,多个线程共享数据使用Runnable接口
一些关于Thread类的API
方法 | 描述 |
---|---|
public static Thread currentThread() | 返回当前的线程 |
public final String getName() | 返回线程名称 |
public final void setPriority(int priority) | 设置线程优先级 |
public void start() | 开始执行线程 |
public static void sleep(long m) | 使目前的线程休眠**m毫秒** |
public final void yield() | 暂停目前的线程,运行其他 线程 |
public void run() | 线程要执行的任务 |
sleep()
线程休眠,会让当前线程处于阻塞状态,指定时间过后,线程就绪状态
yield()
线程礼让,暂停当前正在执行的线程对象,并执行其他线程
yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪之后,有可能还会再次抢到。
join()
join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个线程运行结束,当前线程再由阻塞转为就绪状态
interrupt:中断线程,仅仅发送了一个中断的信号,当碰到wait(),sleep方法时,清除中断标记,抛出异常。(了解)
setDaemon:设置线程为后台(守护)线程。
3、线程状态
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
数据安全问题
-
条件1:多线程并发。
-
条件2:有共享数据。
-
条件3:共享数据有修改的行为。
变量对线程安全的影响
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
结论:
局部变量永远都不会存在线程安全问题。
实例变量在堆中,堆只有1个。 静态变量在方法区中,方法区只有1个。 堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。 成员变量:可能会有线程安全问题。
什么是线程同步?
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作
那相对的线程异步处理就不用阻塞当前线程,而是允许后续操作,直至其他线程将处理完成,并回调此线程。
线程同步的利弊
-
好处:解决了线程同步的数据安全问题
-
弊端:当线程很多的时候,每个线程都会去判断同步上面的这个锁,很耗费资源,降低效率
基于Lock实现
ReentrantLock类实现了Lock,它拥有和synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁,释放锁。
synchronized与Lock的对比
Lock是显示锁,需要自己手动开启和关闭,synchronized是隐式锁,出了作用于自动释放,无需自己释放。
Lock只能锁代码块,synchronized可以锁代码块和方法
使用Lock锁,JVM将花费更少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
如何解决线程安全问题
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样 实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象, 对象不共享,就没有数据安全问题了。)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候 就只能选择synchronized了。线程同步机制。
死锁:
死锁:当多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,从而导致两个或者多个线程都在等待对方释放资源,都停止执行的情况。
什么是“生产者和消费者模式”?
生产者消费者问题是多线程同步的一个经典问题。生产者消费者同时使用一块缓冲区,生产者生产商品放入缓冲区,消费者从缓冲区取出商品。我们需要保证的是,当缓冲区满时,生产者不可生产商品;当缓冲区为空时,消费者不可取出商品。
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
仓库类:
public class Store {
// 库存数量 ,计划最多放入10个
private int count = 0;public synchronized void push() {
if (count == 10) {
System.out.println(Thread.currentThread().getName()+"库存已满:" + count);
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// if(count<10){
count++;
System.out.println(Thread.currentThread().getName()+"正在生产:,现有库存" + count);
// }
this.notify();
}public synchronized void pop() {
if (count == 0) {
System.out.println(Thread.currentThread().getName()+"库存已空:" + count);
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return;
}try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// if(count>0){
count--;
System.out.println(Thread.currentThread().getName()+"正在消费:,现有库存" + count);
// }this.notify();
}}
生产者:Producer
public class Producer implements Runnable {
private Store store;public Producer(Store store) {
this.store = store;
}@Override
public void run() {
for(int i=1;i<=50;i++){
store.push();
}
}
}
消费者:Consumer
public class Consumer implements Runnable {
private Store store;public Consumer(Store store) {
this.store = store;
}@Override
public void run() {
for(int i=1;i<=50;i++){
store.pop();
}
}
}
生产者消费者模式的优点:
1、解耦: 由于有缓冲区的存在,生产者和消费者之间不直接依赖,耦合度降低。
2、支持并发: 由于生产者与消费者是两个独立的并发体,他们之间是用缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区了拿数据即可,这样就不会因为彼此的处理速度而发生阻塞。
3、支持忙闲不均: 缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来 了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。 等生产者的制造速度慢下来,消费者再慢慢处理掉。
sleep方法和wait方法的区别?
1、sleep是Thread类种的方法,wait是Object中的方法。
2、wait必须用在同步代码中,释放锁,让当前线程处于等待状态;sleep不会解锁;
线程池
线程池就是首先创建一些线程,他们的集合称之为线程池。线程池在系统启动时会创建大量空闲线程,程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行结束后线程不会销毁(死亡),而是再次返回到线程池中成为空闲状态,等待执行下一个任务。
创建线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
-
corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
-
设置规则: CPU密集型(CPU密集型也叫计算密集型,指的是运算较多,cpu占用高,读/写I/O(硬盘/内存)较少):corePoolSize = CPU核数 + 1 IO密集型(与cpu密集型相反,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。):corePoolSize = CPU核数 * 2
-
-
maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。默认为Integer.MAX_VALUE,一般设置为和核心线程数一样
-
keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
线程空闲时间,默认为60s,一般设置为默认60s
-
unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
-
workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
-
threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
-
handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略
默认是AbortPolicy,丢弃任务并抛出 RejectedExecutionException 异常。
任务队列(workQueue)
任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:
关闭线程方法:shutdown和shutdownNow的区别
-
shutdown():仅仅是不再接受新的任务,以前的任务还会继续执行
-
shutdownNow():立刻关闭线程池,如果线程池中还有缓存的任务没有执行,则取消执行,并返回这些任务
通过Executor工厂类中的静态方法获取线程池对象
1、通过newCachedThreadPool获取线程池对象
该方式特点是:创建一个默认的线程池对象,里面的线程可重用,且在第一次使用时才创建
2、通过newFixedThreadPool获取线程池对象
该方式特点是:可指定创建线程数,并且可以重复用
3、通过newSingleThreadExecutor获取线程池对象
该方式特点是:只会创建一个线程
三种创建线程池的区别
第一种:newCachedThreadPool:线程的数据是不做限制的,每次有任务来的时候都会以任务优先,性能最大化(也就是服务器压力比较大)
第二种:newFixedThreadPool:可以让压力不那么大,并且可以规定线程的数量,当线程的数量达到指定数量的时候,这个时候就不会再有新的线程了
第三种:newSingleThreadExecutor:绝对的安全,不考虑性能,因为是单线程,永远只有一个线程来执行任务。
Executors 的 功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。