为什么需要并发编程
因为现在的CPU我们大家也都知道,什么几核几线程,各种各样,而我们并发编程的目的是为了让程序运行得更快,这里的更快说的并不是让我们无限制启动更多的线程就能让程序进行最大可能的并发操作,但是我们在进行并发编程的时候,很容易遇到很多的问题,比如说死锁问题,再比如说上下文的切换的问题,这都是问题所在。
实现多线程的几种方式,面试中最简单的题目
说起来这个面试题,很多回答都一样,
- 继承Thread类
- 实现Runnable接口
- 使用线程池
这是很多面试者回答的时候总是回答这三个,但是实际上,实现多线程的方式也不限于这几种方式,还有比如说带返回值的线程实现,定时器实现,内部类实现,这些方式都是可以实现多线程的。那我们今天就先来把这些不常用的方式来梳理一下。
使用匿名内部类的方式实现多线程
其实说实话,这匿名内部类的方式也不能算是一种新的实现方式,只不过是把这个实现方式放到了匿名类里面了,实现的总体内部还是使用的继承 Thread和实现Runnable接口。
案例实现:
public class TestClass { public static void main(String[] args) { // 基于子类的方式 new Thread() { @Override public void run() { while (true) { printThreadInfo(); } } }.start(); // 基于接口的实现 new Thread(new Runnable() { @Override public void run() { while (true) { printThreadInfo(); } } }).start(); } private static void printThreadInfo() { System.out.println("当前运行的线程名为: " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (Exception e) { throw new RuntimeException(e); } } } 实现结果: 当前运行的线程名为:Thread-1 当前运行的线程名为:Thread-0 当前运行的线程名为:Thread-1 当前运行的线程名为:Thread-0 当前运行的线程名为:Thread-1 当前运行的线程名为:Thread-0 当前运行的线程名为:Thread-0 当前运行的线程名为:Thread-1 当前运行的线程名为:Thread-1 当前运行的线程名为:Thread-0 当前运行的线程名为:Thread-0 当前运行的线程名为:Thread-1
其实对于上述手段,大家也肯定都会,那么我们就说说这个定时器实现方式,这个方式实际上是也是大家经常会使用的一种方式,因为我们很多时候都需要在我们不在的情况下进行一些操作,比如说,每天晚上对系统进行一下当天的统计操作什么的。
使用定时器实现
public class TestClass { private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); public static void main(String[] args) throws Exception { // 创建定时器 Timer timer = new Timer(); // 提交计划任务 timer.schedule(new TimerTask() { @Override public void run() { System.out.println("定时任务执行了..."); } }, dateFormat.parse("2020-12-08 20:30:00")); } } 这段代码大家可以复制一下,在你设定好的时间内进行执行
关于多线程的实现方式,阿粉就给大家讲述到这里,毕竟这个东西在你使用的时候,一定是活学活用的,不是一成不变的,需要你看自己的需求来弄。
接下来我们就先从并发编程的线程安全性开始入手,接下来阿粉也会继续给大家更新关于并发编程的各种技术内容,让大家能够尽快的掌握好这个线程安全的问题,
线程的安全性操作
其实对于一个对象来说,他是否是线程安全的,完全取决于他是否被多个线程去访问,而如果要让我们的对象是线程安全的话,那么我们一定要采取一些方式,而方式都有哪些呢?
- 同步机制
- 加锁机制
也就是大家所了解的同步 Synchronized 和加锁的机制。还有就是使用Volatile类型的变量。
也就是说,如果多个线程去访问同一个可变的状态的变量的时候,没有使用合适的同步,那么程序相对来说就会出现错误,而解决方式也有好几种,
- 比如说不在线程之前共享这个变量
- 将状态变量修改成为不可变的的变量
- 在访问状态变量的时候使用同步
而阿粉之前也看过一个图片,就是说他从字节码的角度去分析了线程不安全的操作,看下图
用一个最简单的案例给大家讲解Synchronized,我们手动实现一个线程然后递减,每次输出这个变量,最终看效果图
public class TestClass implements Runnable{ int i = 100; @Override public void run() { // TODO Auto-generated method stub while(true) { if(i>0) { try { Thread.sleep(10);//为了让安全问题明显,我们让线程执行的时间变长,故睡眠10毫秒 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(i); i--; } } } } class Test{ public static void main(String[] args) { TestClass testClass = new TestClass(); Thread t1 = new Thread(testClass); Thread t2 = new Thread(testClass); Thread t3 = new Thread(testClass); t1.start(); t2.start(); t3.start(); } }
不用说大家都知道,结果肯定是乱的一塌糊涂,有来回跳跃的,也有分段执行的,反正就是不是从100到1的,结果大家可以把代码拿过去使用一下自己看看。
那么我们加上Synchronized关键字之后呢?
public class TestClass implements Runnable{ int i = 100; @Override public void run() { while(true) { synchronized (this){ if(i>0) { try { Thread.sleep(10);//为了让安全问题明显,我们让线程执行的时间变长,故睡眠10毫秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(i); i--; } } } } } class Test{ public static void main(String[] args) { TestClass testClass = new TestClass(); Thread t1 = new Thread(testClass); Thread t2 = new Thread(testClass); Thread t3 = new Thread(testClass); t1.start(); t2.start(); t3.start(); } }
大家可以去执行一下运行结果,顺带打印出执行结果,是不是这次就很舒服了,终于看到自己心心念念的从100-1的内容了,而实际上,我们只是通过加上了一个同步的关键字,来实现了线程的安全性操作,让线程同步执行,不再会出现那个不安全的行为,是不是很简单?你学会了么?
下一篇文章阿粉将会带给大家关于另外的一个关键字 Volatile 实现线程安全,并且告诉大家他的可见性,还有原子性。
文献参考 《Java并发编程艺术》 《Java并发编程》