一,宏觀概念
1,進程和線程
- 進程是獨立應用程序,線程是進程的一條執行路徑。
- 一個進程通常有N個線程
2,多線程
指進程中的多個路徑同時執行,主要目的是提高程序效率。
【舉個栗子】:
打開網易雲音樂,可以理解為一個進程,然后點開一首歌曲,這是一個線程,然后在播放歌曲的同時,可以在下邊評論,這就是兩個線程。
3,並發與並行
多線程是針對單核CPU的,也就是並發。
多核CPU的多個核心同時運算稱為並行。
4,多線程的使用場景
多線程的本質是CPU時間片的快速切換,當並發操作次數很大時,可以忽略掉創建線程和線程切換的開銷,但是如果並發量很小,多線程就顯得多此一舉了。
二,多線程創建方式
1.繼承Thread,重寫run方法
在晴朗早晨,和朋友一邊散步一邊聊天.....
CPU交替執行三件事,但切換速度很快,感覺上就是在同時進行......
class ThreadWalk extends Thread
{
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println("Walk。。。。。");
}
}
}
class ThreadTalk extends Thread
{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Talking........");
}
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println("Sunshine.....");
}
ThreadWalk threadWalk = new ThreadWalk();
threadWalk.start();
ThreadTalk threadTalk = new ThreadTalk();
threadTalk.start();
}
}
運行結果:
Sunshine.....
Sunshine.....
Sunshine.....
Sunshine.....
Sunshine.....
Sunshine.....
Sunshine.....
Sunshine.....
Sunshine.....
Sunshine.....
Walk。。。。。
Walk。。。。。
Walk。。。。。
Walk。。。。。
Walk。。。。。
Walk。。。。。
Walk。。。。。
Walk。。。。。
Walk。。。。。
Walk。。。。。
Talking........
Talking........
Talking........
Talking........
Talking........
Talking........
Talking........
Talking........
Talking........
Talking........
Process finished with exit code 0
2. 實現Runnable接口
可繼承多個接口,彌補單繼承的不足
class MyRunnable1 implements Runnable
{
int i=10;
public void run()
{
for(;i>0;)
{
System.out.printf("day"+"\n");
i--;
}
}
}
class MyRunnable2 implements Runnable
{
int j=10;
public void run()
{
for(;j>0;)
{
System.out.printf("night"+"\n");
j--;
}
}
}
public class Main
{
public static void main(String []args)
{
MyRunnable1 runnable1 = new MyRunnable1();
MyRunnable2 runnable2 = new MyRunnable2();
Thread thread1 = new Thread(runnable1);
Thread thread2 = new Thread(runnable2);
thread1.start();
thread2.start();
for(int k=0;k<10;k++)
{
System.out.printf("main thread..."+"\n");
}
}
}
運行結果:
main thread...
main thread...
main thread...
main thread...
main thread...
night
night
night
night
night
night
night
night
night
night
day
day
day
day
day
day
day
day
day
day
main thread...
main thread...
main thread...
main thread...
main thread...
3.匿名內部類實現多線程
public class Main
{
public static void main(String []args)
{
Thread thread = new Thread(
new Runnable() {
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println("no Name thread。。。。");
}
}
}
);
thread.start();
for(int k=0;k<10;k++)
{
System.out.printf("main thread..."+"\n");
}
}
}
運行結果:
main thread...
main thread...
main thread...
main thread...
main thread...
main thread...
main thread...
no Name thread。。。。
no Name thread。。。。
no Name thread。。。。
no Name thread。。。。
main thread...
main thread...
main thread...
no Name thread。。。。
no Name thread。。。。
no Name thread。。。。
no Name thread。。。。
no Name thread。。。。
no Name thread。。。。
4,實現Callable接口
public class CreatThreadDemo4 implements Callable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CreatThreadDemo4 demo4 = new CreatThreadDemo4();
FutureTask<Integer> task = new FutureTask<Integer>(demo4); //FutureTask最終實現的是runnable接口
Thread thread = new Thread(task);
thread.start();
System.out.println("我可以在這里做點別的業務邏輯...因為FutureTask是提前完成任務");
//拿出線程執行的返回值
Integer result = task.get();
System.out.println("線程中運算的結果為:"+result);
}
//重寫Callable接口的call方法
@Override
public Object call() throws Exception {
int result = 1;
System.out.println("業務邏輯計算中...");
Thread.sleep(3000);
return result;
}
}
5,使用線程池
public class CreatThreadDemo6 {
public static void main(String[] args) {
//創建一個具有10個線程的線程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
long threadpoolUseTime = System.currentTimeMillis();
for (int i = 0;i<10;i++){
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"線程執行了...");
}
});
}
long threadpoolUseTime1 = System.currentTimeMillis();
System.out.println("多線程用時"+(threadpoolUseTime1-threadpoolUseTime));
//銷毀線程池
threadPool.shutdown();
threadpoolUseTime = System.currentTimeMillis();
}
}
6,使用定時器Timer
public class CreatThreadDemo5 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定時器線程執行了...");
}
},0,1000); //延遲0,周期1s
}
}
7,使用Java8新特性stream
public class CreatThreadDemo7 {
public static void main(String[] args) {
List<Integer> values = Arrays.asList(10,20,30,40);
//parallel 平行的,並行的
int result = values.parallelStream().mapToInt(p -> p*2).sum();
System.out.println(result);
//怎么證明它是並發處理呢
values.parallelStream().forEach(p-> System.out.println(p));
}
}
三,守護線程
主線程:main方法所在線程
用戶線程(子線程):main方法中創建的子線程
守護線程:和main方法一起銷毀的線程,比如說GC線程
非守護線程:main方法銷毀依然執行的線程
thread.setDaemon(true);可設置線程為守護線程
class Main
{
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i <10 ; i++) {
System.out.println("子線程...");
}
}
});
// thread.setDaemon(true);
thread.start();
for (int i = 0; i <10 ; i++) {
System.out.println("main線程.....");
}
System.out.println("主線程銷毀....");
}
}
四,join方法
在線程中調用另一個線程的join方法會暫時停止當前線程,將CPU資源給另一個線程先使用。
class Main
{
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println("子線程...");
}
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i <10 ; i++) {
System.out.println("main線程.....");
}
System.out.println("主線程銷毀....");
}
}
子線程...
子線程...
子線程...
子線程...
子線程...
子線程...
子線程...
子線程...
子線程...
子線程...
main線程.....
main線程.....
main線程.....
main線程.....
main線程.....
main線程.....
main線程.....
main線程.....
main線程.....
main線程.....
主線程銷毀....
六,線程安全問題
當多個線程同時共享,同一個全局變量或靜態變量,進行寫的操作時,可能會發生數據沖突問題,也就是線程安全問題。但是進行讀操作是不會發生數據沖突問題。
本質上是數據一致性問題。
Synchronized
- 關鍵字 synchronized可以保證在同一個時刻,只有一個線程可以執行某個方法或者某個代碼塊
- synchronized可保證一個線程的變化(主要是共享數據的變化)被其他線程所看到(保證可見性)
非靜態同步方法
經典火車售票問題
class ThreadDemo implements Runnable
{
private static int count = 100;
//非靜態同步方法
public synchronized void sale()
{
if(count>0)
{
count--;
System.out.println(Thread.currentThread().getName()+"剩余票數"+count);
}
}
@Override
public void run() {
while(count>0)
{
sale();
}
}
}
public class Main{
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
Thread t1 = new Thread(threadDemo,"Window1");
Thread t2 = new Thread(threadDemo,"WIndow2");
t1.start();
t2.start();
}
}
靜態同步方法
class ThreadDemo implements Runnable
{
private static int count = 100;
//靜態同步方法
public synchronized static void sale()
{
if(count>0)
{
count--;
System.out.println(Thread.currentThread().getName()+"剩余票數"+count);
}
}
@Override
public void run() {
while(count>0)
{
sale();
}
}
}
public class Main{
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
Thread t1 = new Thread(threadDemo,"Window1");
Thread t2 = new Thread(threadDemo,"WIndow2");
t1.start();
t2.start();
}
}
同步代碼塊
class ThreadDemo implements Runnable
{
private static int count = 100;
//同步代碼塊
public void sale()
{
synchronized(this)
{
if(count>0)
{
count--;
System.out.println(Thread.currentThread().getName()+"剩余票數"+count);
}
}
}
@Override
public void run() {
while(count>0)
{
sale();
}
}
}
public class Main{
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
Thread t1 = new Thread(threadDemo,"Window1");
Thread t2 = new Thread(threadDemo,"WIndow2");
t1.start();
t2.start();
}
}
非靜態同步方法和靜態同步方法的區別
前者同步在對象層面上,后者同步在類的層面上
非靜態同步方法和同步代碼塊的區別
前者在方法上,后者在方法內部。范圍不同。
通常來說,范圍越大,性能越差。
死鎖
同步中嵌套同步,導致鎖無法釋放。
有兩個線程並行執行,當線程1拿到了obj1鎖,線程2拿到了obj2鎖,就會出現互相等待的情況。
線程1拿着obj1等待obj2,線程2拿着obj2等着obj1.
class ThreadDemo implements Runnable {
private static int count = 100;
private Object obj1 = new Object();
private Object obj2 = new Object();
public void sale() {
synchronized (obj1) {
synchronized (obj2) {
if (count > 0) {
count--;
System.out.println(Thread.currentThread().getName() + "剩余票數" + count);
}
}
}
}
@Override
public void run() {
synchronized (obj2) {
synchronized (obj2) {
sale();
}
}
}
}
public class Main{
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
Thread t1 = new Thread(threadDemo,"Window1");
Thread t2 = new Thread(threadDemo,"WIndow2");
t1.start();
t2.start();
}
}
多線程三大特性
原子性
一個操作或者多個操作要么全部執行,要么都不執行;在多線程中,原子性主要體現在數據一致性上。
可見性
當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
有序性
程序執行的順序按照代碼的先后順序執行。
七,Java內存模型
Java內存模型(JMM)主要目標是定義程序中共享變量(線程共享)的訪問規則。
JMM規定線程之間的共享變量存儲在主內存中,每個線程都有一個本地內存(工作內存),本地內存存儲了共享變量的副本。
Volatile關鍵字
volatile是一種輕量級的同步機制,可以保證可見性【及時將修改的變量刷新到主內存中】,但不能保證原子性,並且禁止重排序。
應用場景:全局共享變量
Volatile的可見性:一旦某個線程修改了該被volatile修飾的變量,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,可以立即獲取修改之后的值。
Synchronized和Volatile的區別
JMM關於synchronized的兩條規定:
- 線程加鎖時,將清空工作內存中共享變量的值(本地內存已經有備份了)
- 線程解鎖前,必須把本地內存的最新值刷新到主內存
(注意:加鎖與解鎖需要是同一把鎖)
通過以上兩點,可以看到synchronized能夠實現可見性。同時,由於synchronized具有同步鎖,所以它也具有原子性
Synchronized同時具有原子性和可見性,但效率低
Volatile僅具有可見性,但效率高
重排序
重排序是指CPU對代碼的優化,但是不會對有依賴關系代碼做重排序。
比如說
int x = 3; -----1
int y = 5; -----2
int z = x+y; -----3
1和2的執行順序可能會發生改變,但一定在3之前。
【舉個栗子】
定義兩個靜態變量,創建一個讀線程,一個寫線程。
如果是按照順序執行的話,讀線程會讀到從1到5連續或者相同的數字。
public class Main {
private static int a=0;
private static int b=0;
public static void main(String[] args) {
Thread t1_write = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true)
{
a = 1; // 這里可能發生重排序
b = 1; // 這里可能發生重排序
a = 2; // 這里可能發生重排序
b = 2; // 這里可能發生重排序
a = 3; // 這里可能發生重排序
b = 3; // 這里可能發生重排序
a = 4; // 這里可能發生重排序
b = 4; // 這里可能發生重排序
a = 5; // 這里可能發生重排序
b = 5; // 這里可能發生重排序
a = 4; // 這里可能發生重排序
b = 4; // 這里可能發生重排序
a = 3; // 這里可能發生重排序
b = 3; // 這里可能發生重排序
a = 2; // 這里可能發生重排序
b = 2; // 這里可能發生重排序
}
}
});
Thread t2_read = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while(true)
{
System.out.println("a="+a);
System.out.println("b="+b);
System.out.println("=========================");
}
}
});
t1_write.start();
t2_read.start();
}
}
運行結果有的連續,有的並不連續,說明在執行讀線程的時候,CPU進行了重排序。但是重排序只是一種情況。也有可能a=2時,執行讀線程,輸出b的時候a仍在執行,這種情況也會導致不連續。
如何用代碼驗證重排序目前還沒有想出可行的方法,歡迎小伙伴們提出解決方法。_