多线程的三种实现方式:
1、继承Thread类,重写run方法
两个问题:
1)为什么要重写run方法?
因为run()是用来封装被线程执行的代码
2)run()方法和start()方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程。
start():启动线程,然后由JVM调用此次线程的run()方法
2、实现Runnable接口,重写run()方法 ------------无返回值
3、实现Callable接口,重写call()方法,通过中介FutureTask来将获取结果----------有返回值
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白"+i);
}
return "答应";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程开启后需要执行里面的call()方法
MyCallable mc=new MyCallable();
//可以获取线程执行完毕的结果,也可以作为参数传递给Thread对象
FutureTask ft=new FutureTask(mc);
//创建线程对象
Thread t1=new Thread(ft);
//设置线程的名字----线程是有默认名字的
t1.setName("大白");
//开启线程
t1.start();
//获取线程的结果以及线程的名字
System.out.println(ft.get()+t1.getName());
}
三种实现多线程方式的对比:
线程的优先级,可以通过Thread中的setPriority()方法设置。
扩展:守护线程–当主线程停止,守护线程也不再执行,可以通过Thread中的setDaemon(true)设置线程为守护线程。
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
return "线程执行完毕";
}
}
public class MyCallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//优先级:1-10
MyCallable mc=new MyCallable();
MyCallable mc2=new MyCallable();
FutureTask ft=new FutureTask(mc);
FutureTask ft2=new FutureTask(mc2);
Thread t1=new Thread(ft);
Thread t2=new Thread(ft2);
t1.setName("大白");
//------------------------------------- 设置t1优先级为10
t1.setPriority(10);
t2.setName("小黑");
//---------------------------------------设置t2优先级为1
t2.setPriority(1);
t1.start();
t2.start();
}
}
线程的安全问题:
锁多条语句操作共享数据,可以使用共享代码块实现
格式:
synchronized(任意对象){
多条语句操作共享数据的代码
}
默认情况下是打开的,当有线程进去就会自动关闭,当线程执行完,锁才会自动打开。
同步的好处与弊端
利:解决了多线程数据的安全问题
弊:当线程很多时,每个线程都要去判断同步上的锁,耗费资源且降低效率
Lock锁:
synchronized无法直接看到在哪上了锁,在哪释放锁,为了更清晰的表达加锁和释放锁,JDK5后提供了一个新的锁对象Lock
Lock实现提供比synchronized更为广泛的锁定操作
void lock():获取锁
void unlock(): 释放锁
Lock接口是不能直接实例化的,这里采用它的实现类ReentrantLock来实例化
ReentrantLock lock=new ReentrantLock();
lock.lock();
代码块;
lock.unlock(); //一般情况下,释放锁放到finally里执行,避免出异常锁还没有释放
synchronized和lock锁都属于悲观锁,下面引入其他博主关于悲观锁和乐观锁的对比,应付面试必备!
synchronized底层实现
悲观锁&乐观锁区别
多线程和MQ的思考?
偶然想到多线程可以执行任务,MQ也可以执行任务,两者的选择有何不同?
个人的理解,计算的时候,一般是每一步的计算量很大,分成几步去算的话,使用多线程效率就变高了。而MQ一般是削峰填谷的时候用。在线程池里有个阻塞队列,其作用就是为了在面对用户数多,使用多线程时可能会爆时的一种解决策略,用来控制核心线程数、最大数量。在多线程里涉及AQS相关的问题。
Volatile关键字
小案例:男女生结婚基金,基金相当于共享数据,男女生相当于两条线程;
public class Boy extends Thread {
@Override
public void run() {
try {
sleep(10); //让男孩线程睡上一会
Money.money=9000;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public class Girl extends Thread {
@Override
public void run() {
while (Money.money==10000){
}
System.out.println("结婚基金不是10000了");
}
}
}
public class Money {
public static int money=10000;
}
public class Test { //结果为程序一直停不下来
public static void main(String[] args) {
Girl girl=new Girl();
girl.setName("女孩");
Boy boy=new Boy();
boy.setName("男孩");
girl.start();
boy.start();
}
}
上述图小结:
1)堆内存是唯一的,每一个线程都有自己的线程栈;
2)每一个线程在使用堆里面变量的时候,都会拷贝一份到变量的副本中;
3)在线程中,每一次使用都是从变量的副本中获取的
解决上述问题即用到了关键字volatile,它会强制线程每次在使用时,都会看一下共享区域最新的值!但是它不能保证原子性!
synchronized同步代码块也可以解决该问题,但其原理不同,如下
1、线程获取锁 2、情况变量副本 3、拷贝共享数据最新的值到变量副本中 4、执行代码 5、将修改后的变量副本的值赋值给共享数据 6、释放锁
注意:Volatile关键字不能保证原子性,因为当A线程拿到数据修改后如果还没有写到共享数据中,线程B从共享数据中拿到的还是原来的旧数据,加synchronized同步代码块虽然可以解决共享数据修改问题,但效率较慢,jdk5后提供了原子类AtomicInteger(实现原理:自旋锁+CAS算法,图如下)
小结:
CAS算法:在修改共享数据时,把原来的数据记录下来,如果现在内存里的值和旧值相等,证明没有其他线程操作过内存,则修改成功,如果不相等,说明其他线程修改过,则重新获取最新的内存值,这个重新获取的过程就称之为自旋。
Volatile和Synchronized有什么区别?Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加Volatile?
1、Synchronized关键字用来加锁,Volatile只是保证变量的线程可见性,通常适用于一个线程写,多个线程读的场景;
2、不能,Volatile关键字只能保证线程可见性,不能保证原子性;
3、Volatile防止指令重排。DCL中,防止高并发情况下,指令重排造成的线程安全问题。
在并发情况下,常见的面试题有ConcurrentHashMap为什么能够保证数据添加的安全性,此处引入一篇讲解其源码还不错的博客ConcurrentHashMap 1.7和1.8区别
下面的程序验证了两个事实:
1、程序的执行是顺序性的;
2、主线程对子线程抓取异常,不生效,因为不是同一个线程;
public class Demo {
public static void main(String[] args) throws InterruptedException {
try {
new Thread(()->{
for (int i = 0; i <10; i++) {
System.out.print(i);
}
throw new RuntimeCryptoException(); //自定义异常也可以正常抛出
}).start();
} catch (Exception e) {
System.out.println("此处异常不会打印,因为是主线程的");
}finally {
System.out.println("这一行先打印,因为是先运行主线程");
}
Thread.sleep(1000); //此处睡眠的是主线程
}
==================打印台输出结果为============================
这一行先打印,因为是先运行主线程
0123456789
Exception in thread "Thread-0" org.bouncycastle.crypto.RuntimeCryptoException
at com.Demo.lambda$0(Demo.java:15)
at java.lang.Thread.run(Unknown Source)
}
在并发工具类CountDownLatch中,翻阅到其是使用AQS实现,上链接AQS详解