并發編程把我整的是服服氣氣的了
本文轉載自微信公眾號「Java極客技術」,作者鴨血粉絲 。轉載本文請聯系Java極客技術公眾號。
阿粉因為原來的編程習慣,已經很久沒有去考慮并發的問題了,結果之前在面試的問題就回答的不是很完善,而阿粉也用心學習了并發編程這一塊的所有內容,一起來分享給大家。
為什么需要并發編程
因為現在的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 實現線程安全,并且告訴大家他的可見性,還有原子性。