在自己还是很弱鸡的时候,经常面试被问到创建线程的几种方式。那个时候想的是不就2种吗,一个实现runable接口,一个继承thread类。发现了自己为什么不能跟面试官聊下去的原因了~~目前还是个弱鸡,但是还是发现了很多种方法。总结了一下,无非是变相的使用Thread 和线程池。还有一个lambda的(并行流)。
继承Thread类
package com.example.demo.demo1;
public class Demo1 extends Thread {
public Demo1(String name) {
super(name);
}
@Override
public void run() {
while (!interrupted()){
System.out.println(getName()+"线程执行了");
}
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread thread = new Demo1("hah");
thread.setDaemon(true);//将线程设置为守护线程,随着main线程的存在而存在
thread.start();
thread.interrupt();//线程的中断 不要用stop 因为stop并不会释放锁等资源
}
}
实现Runable接口
package com.example.demo.demo1;
public class Demo2 implements Runnable {
@Override
public void run() {
while (true){
System.out.println("线程正在运行");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new Demo2());
thread.start();
}
}
匿名内部类
package com.example.demo.demo1;
public class Demo3 {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
System.out.println("线程执行");
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行2");
}
}).start();
}
}
带返回值的线程
package com.example.demo.demo1;
import org.omg.PortableServer.THREAD_POLICY_ID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo4 implements Callable<Integer> {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//实例化实现callable接口对象
Demo4 demo4 = new Demo4();
//FutureTask实现RunnableFuture接口,RunnableFuture是Runable接口的子类
FutureTask<Integer> task = new FutureTask<>(demo4);
//将FutureTask以Runable传入
Thread thread = new Thread(task);
thread.start();
Integer a = task.get();
System.out.println(a);
}
@Override
public Integer call() throws Exception {
Thread.sleep(3000);
return 1;
}
}
定时器
package com.example.demo.demo1;
import java.util.Timer;
import java.util.TimerTask;
public class Demo5 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("正在执行");
}
},0,1000);
}
}
线程池
package com.example.demo.demo1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo6 {
public static void main(String[] args) {
//newCachedThreadPool会自动回收已经用完的线程资源
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i = 0 ;i<10;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程执行");
}
});
}
//线程池需要关闭
executorService.shutdown();
}
}
常用的几个线程池
- (1)newCachedThreadPool
- (2)newFixedThreadPool
- (3)newSingleThreadExecutor
- (4)newScheduleThreadPool
1.创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
2.创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
3.创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
4.创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
spring实现多线程
lambda表达式
package com.example.demo.demo1;
import java.util.ArrayList;
import java.util.List;
public class Demo7 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.parallelStream().forEach(System.out::println);
Integer b =list.parallelStream().mapToInt(a ->a).sum();
System.out.println(b);
}
}
多线程情况下碰得到的问题
1.线程安全性问题
2.活跃性问题(死锁,饥饿,活锁)
3.性能问题(CPU需要给线程分配时间片,线程间的切换 需要记录执行的位置等操作(类似计数器))
活跃性问题
1.死锁(多个线程互相竞争同一个资源)
2.饥饿(由于线程优先级问题,导致线程优先级低的无法抢到CPU的执行时间片,导致一直无法执行)
3.活锁(跟死锁相反,2个都是互相让步CPU资源,然后切换的时候又竞争到同一片时间片)
饥饿和公平
- 高优先级吞噬所有低优先级的CPU的时间片
- 线程被永久堵塞在一个等待进入同步块的状态
- 等待的线程永远不被唤醒
如何尽量避免饥饿问题
- 合理的设置优先级
- 使用锁来代替synchronized
线程安全性问题
- 多线程环境下
- 多线程共享一个资源
- 对资源进行非原子性操作
偏向锁
每次获取锁和释放锁会浪费资源
很多情况下,竞争锁并不是多线程,而是由一个线程在使用
只有单线程访问同步代码块适用
轻量级锁
- 自旋
- 多个线程同时访问
Synchronized jvm原理
锁的信息存放在对象头中。
对象头中的信息
- Mark Word
- Class Metadata Address
- 偏向锁
- 轻量级锁
- 重量级锁
Synchronized解决安全问题
内置锁(Synchronized)保证线程原子性,当线程进入方法时候,自动获取锁,一旦锁被其他线程获取到,其他的线程就会进行等待。
锁的特征:只能有一个线程进行使用。
怎么去释放锁,程序执行完毕的之后,就会把锁释放。
但是他会降低程序的运行效率
锁的资源的竞争,重入锁
内置锁也是互斥锁
使用方式:同步方法,同步代码块
volatile
- Volatile称之为轻量级锁,被volatile修饰的变量,在线程之间是可见的。
- 可见:一个线程修改了这个变量的值,在另外一个线程能够读到这个线程修改后的值。
- Synchronized除了线程之间的互斥意外,还有一个非常大的作用,就是保证了可见性。
- 只要是全局共享变量,全部都加上volatile
volatile与synchronized区别
volatile作用:可以保证可见性,但是不能保证原子性,禁止重排序
synchronized:既可以保证原子性还可以保证原子性
重排序概念
cpu对代码实现进行优化,不会对有依赖关系做重排序
代码执行的顺序可能会发生改变,但是执行的结果不会发生任何改变。
多线程
什么是数据依赖关系?
int a = 1;
int b = 2;
int c = a*b;
a b没有依赖关系,可能先执行b
as-if-serial规则
serial 不管怎么去做重排序,但是目的是提高并行度,但是不能影响到正常的结果。
重排序,在多线程情况下遇到。
JDK提供的原子类原理及使用
- 原子更新基本类型(AtomicInteger…)
- 原子更新数组(AtomicIntegerArray…)
- 原子更新抽象类型(AtomicReference…)
- 原子更新字段
原理:CAS
Lock接口的使用
Lock li = new ReentrantLock();
li.lock();
li.unlock();
优点
- Lock接口需要显示的获取和释放锁,可以使代码变得更加灵活
- 使用Lock可以方便的实现公平性
- 非阻塞的获取锁
- 能被中断的获取锁
- 超时获取锁
ThreadLocal
什么是Threadlocal?给每个线程提供鞠躬变量,解决线程安全问题。
ThreadLocal底层是一个Map集合。
java内存模型 jmm多线程相关
jmm决定一个线程对共享变量的写入时,能够对另一个线程是否可见。
主内存:共享存储变量
本地内存:共享变量的副本
多线程之间如何实现通讯?
wait
notify
CountDownLatch(计数器)
- CountDownLatch 类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量 。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
CyclicBarrier(栅栏)
- CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。
- CyclicBarrier就象它名字的意思一样,可看成是个障碍, 所有的线程必须到齐后才能一起通过这个障碍。
- CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。
Semaphore(信号量)
- Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞
- Semaphore可以用来构建一些对象池 ,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。
- 它的用法如下:
- availablePermits函数用来获取当前可用的资源数量
- wc.acquire(); //申请资源
- wc.release();// 释放资源
线程池的目的
- 降低资源消耗(重复利用已创建的线程降低线程创建和线程的摧毁的消耗)
- 提高响应效率 当任务到达时,任务可以不需要等到线程创建就能立即执行,方便管理 。
- 方便管理
为什么线程池要用阻塞队列?
能够等待核心线程完成任务,然后复用线程
合理配置线程池
原则:CPU密集。IO密集
CPU密集:该任务需要大量运算,而没有阻塞情况,CPU全速运行。配置线程数=cpu的核数
阻塞产生的原因:请求,读数据库,循环
IO密集:该任务需要大量IO操作,产生IO阻塞。多配置线程数,2*CPU的核数(最大线程数)