JAVA筆記_Thread 多線程


基本概念

一個process可以切割成很多thread,而thread被開啟時也不一定會執行,其調用與順序需要依靠CPU安排,稱為排程

三種可以實現多線程的方式

方法一 extends Thread

  1. class繼承Thread類別
  2. override run()
  3. 再建立物件使用 run()/start()

run()/start()差異

e.g. 以start()來啟動線程

public class Thread1 extends Thread{
    public void  run(){
        for(int i=0;i<1000;i++){
            System.out.println("Thread1 is running now!");
        }
    }

    public static void main(String args[]){
        Thread1 t = new Thread1();
        t.start();

        for(int i=0;i<1000;i++){
            System.out.println("main is running now!");
        }
    }
}

輸出是交錯的,表示main,t兩個thread是交替執行,符合start()的原則

e.g. 多線程執行

public class Thread2 extends Thread{
    private String name;
    public Thread2(String name){
        this.name=name;
    }
    public void run(){
        System.out.println(name+" is running");
    }

    public static void main(String args[]){
        Thread2 t1= new Thread2("thread1");
        Thread2 t2= new Thread2("thread2");
        Thread2 t3= new Thread2("thread3");

        t1.start();
        t2.start();
        t3.start();
    }
}

輸出為
thread1 is running
thread3 is running
thread2 is running
表示開啟線程順序跟執行順序無關, 由CPU排程影響

方法二 implements Runnable

extends Thread方式雖然方便,但是由於java的單繼承原則造成侷限
因此有了implements的方式出現

  1. class implements Runnable介面
  2. override run()
  3. 創建Thread物件時以class作為參數,使用start()啟動

以上面兩個程式作為更改

public class Thread3 implements Runnable{
    public void  run(){
        for(int i=0;i<1000;i++){
            System.out.println("Thread1 is running now!");
        }
    }

    public static void main(String args[]){
        //Thread1 t = new Thread1();
        //t.start();
        Thread3 thread3=new Thread3();
        new Thread(thread3).start();

        for(int i=0;i<1000;i++){
            System.out.println("main is running now!");
        }
    }
}
public class Thread4 implements Runnable{
    private String name;
    public Thread4(String name){
        this.name=name;
    }
    public void run(){
        System.out.println(name+" is running");
    }

    public static void main(String args[]){
        /*
        Thread2 t1= new Thread2("thread 1");
        Thread2 t2= new Thread2("thread 2");
        Thread2 t3= new Thread2("thread 3");

        t1.start();
        t2.start();
        t3.start();
        */

        Thread4 t1= new Thread4("thread 1");
        Thread4 t2= new Thread4("thread 2");
        Thread4 t3= new Thread4("thread 3");

        new Thread(t1).start();
        new Thread(t2).start();
        new Thread(t3).start();
    }
}

e.g.龜兔賽跑
1.烏龜兔子共用同一線程(跑道)
2.兔子會睡覺

public class Race implements Runnable{

    //只有一個贏家
    private static String winner;  

    public void run(){
        for(int i=0;i<=10;i++){

            if(winner!=null) break;
            else{
                if(Thread.currentThread().getName().equals("兔子")){
                    try{
                        Thread.sleep(1);   //模擬兔子睡覺
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }

                System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");

                if(i==10){
                    winner=Thread.currentThread().getName();
                    System.out.println("winner: "+winner);
                }          
            }
        }
    }

    public static void main(String args[]){
        Race race=new Race();

        new Thread(race,"烏龜").start();
        new Thread(race,"兔子").start();
    }
}

烏龜-->跑了0步
烏龜-->跑了1步
烏龜-->跑了2步
烏龜-->跑了3步
烏龜-->跑了4步
烏龜-->跑了5步
烏龜-->跑了6步
烏龜-->跑了7步
烏龜-->跑了8步
烏龜-->跑了9步
兔子-->跑了0步
烏龜-->跑了10步
winner: 烏龜
兔子-->跑了1步

<講解> Thread.currentThread() 很類似於class中指定本物件this的感覺

靜態代理模式

本位類別專注於自己要處理的事情,而將衍伸的任務都交由代理類別來處理
e.g.自己要結婚,但是婚禮流程及規劃由婚禮公司處理

模式:

  • 本位及代理class都要implements同一interface
  • 代理class要代理本位class

e.g.

public class StaticProxy {

    public static void main(String args[]){

        new company(new person()).getMarry();
        //new Thread( ()-> System.out.println("我愛你") ).start();
    }

}

interface Marry{
    void getMarry();
}

class person implements Marry{
    public void getMarry(){
        System.out.println("get marry is happy!");
    }
}

class company implements Marry{

    private Marry customer;

    public company(Marry customer){
        this.customer=customer;
    }

    public void getMarry(){
        before();                  //代理class
        this.customer.getMarry();  //本位class
        after();                   //代理class
    }

    private void before(){
        System.out.println("結婚之前,布置現場");
    }

    private void after(){
        System.out.println("結婚之後,收拾現場");
    }
}

其中

  new company(new person()).getMarry();
//new Thread( ()-> System.out.println("我愛你") ).start();

此處表明了Thread的原理,並且以代理類別進行模擬

Thread 五大狀態及控制語法

一個Thread的誕生到結束會經過以下五大階段:

  1. new : Thread t = new Thread(); 線程創立
  2. 就緒 : 使用start()方法,但是這是代表Thread準備好被執行,但是不一定是馬上,需要CPU去調度順序跟執行時間
  3. 運行 : CPU開始執行,此刻Thread才能算是真正啟動
  4. 阻塞 : sleep,wait等方法使thread不繼續往下執行,阻塞事件結束後進入就緒狀態等待CPU調度
  5. 死亡 : thread執行完畢,並且不能再次啟動

觀察線程狀態

thread.getState(); 可以查看thread當前的線程狀況

e.g

public class Getstate {
    public static void main(String args[]) throws Exception{
        Thread t= new Thread(()->{
            try{
                for(int i=0;i<5;i++){
                    Thread.sleep(1000);
                }
                System.out.println("//////");
            }catch(Exception e){
                e.printStackTrace();
            }
        });

        //觀察狀態
        Thread.State state = t.getState();
        System.out.println(state); //NEW

        t.start();
        state=t.getState();
        System.out.println(state); //RUN

        while(state!=Thread.State.TERMINATED){ //thread不中止就一直輸出狀態
            Thread.sleep(100);
            state=t.getState();
            System.out.println(state);
        }
    }

}

NEW
RUNNABLE
TIMED_WAITING
...
TIMED_WAITING
//////
TERMINATED

優先權

java優先權參數設置為1-10, 1最高 10 最低
然而在執行順序上優先權參數越小是越有可能優先執行
但是主要還是要依照CPU排班所決定

thread.getPriority(); 可以取得優先權參數
thread.setPriority(); 可以修改優先權參數

e.g.

public class Priority {
    public static void main(String args[]){

        thread t= new thread();

        Thread t1 =new Thread(t,"thread1");
        Thread t2 =new Thread(t,"thread2");
        Thread t3 =new Thread(t,"thread3");
        Thread t4 =new Thread(t,"thread4");

        //先設置好優先權,再啟動
        t1.start();

        t2.setPriority(1);
        t2.start();

        t3.setPriority(10);
        t3.start();

        t4.setPriority(8);
        t4.start();
    }
}

class thread implements Runnable{
    public void run(){
        System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
    }
}

thread3--->10
thread4--->8
thread2--->1
thread1--->5

stop語法

使執行中的thread停下進入阻塞狀態

  • 建議利用for次數和立flag --> 讓thread可以自己自然的停止
  • 不建議使用內建的stop/destroy等過時方法
public class Stop implements Runnable{

    //1.設立flag
    private boolean flag=true;

    public void run(){
        int i=0;
        while(flag){
            System.out.println("run thread"+i++);
        }
    }

    //2.設置一個方法可以停止thread(轉換flag)
    public void stop(){
        this.flag=false;
    }

    public static void main(String args[]){
        Stop s= new Stop();
        new Thread(s).start();

        for(int i=0;i<1000;i++){
            System.out.println("main"+i);
            if(i==900){
                s.stop();
                System.out.println("the test is over!");
            }
        }
    }
}

sleep語法

將當前的thread進入指定的阻塞時間, 然後再進入就緒狀態

  • 可以模擬網路的延遲實際狀況
  • 可以實作計時等功能

e.g.

public class Sleep {

    public static void main(String args[]){
        try{
            tenDown();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public static void tenDown() throws Exception{
        int num=10;
        while(true){
            Thread.sleep(1000);
            System.out.println(num--);
            if(num<=0) break;           
        }
    }
}

yield語法

當一個已經正在執行的thread使用yield方法,表示其會重新回到就緒狀態,跟其他就緒狀態thread經由CPU挑選的公平競爭,但是禮讓不一定會成功,因為是看CPU排班機制來決定

e.g.

public class Yield {
    public static void main(String args[]){
        myYield y=new myYield();
        new Thread(y,"a").start();
        new Thread(y,"b").start();   
    }
}


class myYield implements Runnable{
    public void run(){
        System.out.println(Thread.currentThread().getName()+"線程開始執行");
        Thread.yield();  //禮讓
        System.out.println(Thread.currentThread().getName()+"線程停止執行");
    }
}

a線程開始執行
b線程開始執行
b線程停止執行
a線程停止執行
-------------------->照理說應是
a線程開始執行
b線程開始執行
a線程停止執行
b線程停止執行
但是b回到就緒狀態時又被CPU選中,因此禮讓失敗

join語法

直接插隊執行自己的線程,讓其他線程強制進入阻塞狀態

public class Join implements Runnable{
    public void run(){
        for(int i=0;i<1000;i++){
            System.out.println("thread vip is here "+i);
        }
    }

    public static void main(String args[]) throws InterruptedException{
        Join j=new Join();
        Thread thread = new Thread(j);
        thread.start();

        for(int i=0;i<500;i++){
            if(i==100) thread.join();
            System.out.println("main "+i);
        }
    }
}

main 99 -->join 直接開搶
thread vip is here 0
...
thread vip is here 998
thread vip is here 999
main 100
main 101
main 102

Daemon 守護線程

java中thread分為守護線程及用戶線程,預設狀況下所有thread都是用戶線程
而java只會在用戶線程結束時代表執行完成,而不理會守護線程

thread.setDaemon(true); 將線程設定為守護線程, 一般預設為false

e.g.

public class Daemon {
    public static void main(String args[]){
        God god =new God();
        Thread thread=new Thread(god);

        // 程序默認所有thread為用戶線程,也就是false
        thread.setDaemon(true);

        Thread person = new Thread(()->{
            for(int i=0;i<30000;i++){
                System.out.println("Happy living");
            }
            System.out.println("Goodbye World!");
        });

        thread.start();
        person.start();
    }
}

class God implements Runnable{
    public void run(){
        while(true){
            System.out.println("God bless you");
        }
    }
}

Happy living
God bless you
God bless you
Happy living
Happy living
God bless you
Happy living
Goodbye World! --->code已經執行完畢準備要關(關的延遲過程讓守護線程可以活動一陣子)
God bless you
God bless you
God bless you

同步問題

當多個thread因為同時或是極短時間內要共用同個限量的資源,就會造成同個資源同時分給不同thread的假象,形成不安全的程序

e.g.1 搶票

// 3 threads to controll same resource
public class Buyer {
    public static void main(String args[]){
        BuyTicket bt = new BuyTicket();

        new Thread(bt,"t1").start();
        new Thread(bt,"t2").start();
        new Thread(bt,"t3").start();
    }
}

class BuyTicket implements Runnable{
    private int t=10;
    boolean flag=true;

    public void run(){
        while(flag){
            buy();
        }
    }

    private void buy() {
        if(t<=0){
            flag=false;
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"拿到"+t--);
    }
}

t3拿到10
t2拿到9 ---> 重複拿到
t1拿到9 ---> 重複拿到
t2拿到8
t3拿到7
t1拿到6
t2拿到5
t3拿到4
t1拿到3
t2拿到2
t1拿到1
t3拿到0

e.g.2

public class Bank {
    public static void main(String args[]){
        Account a = new Account(100, "wedding fund");

        Drawbank you = new Drawbank(a,50,"you");
        Drawbank wife = new Drawbank(a,100,"wife");

        you.start();
        wife.start();
    }
}

class Account{
    int money;
    String name;
    public Account(int m,String n){
        money=m;
        name=n;
    }
}

class Drawbank extends Thread{
    Account account;
    int drawMoney;

    public Drawbank(Account account,int drawMoney,String name){
        super(name);
        this.account=account;
        this.drawMoney=drawMoney;
    }

    public void run(){
        if(account.money<drawMoney) System.out.println(this.getName()+"取錢時餘額不足");
        else{
            // sleep凸顯現實的問題
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.money=account.money-drawMoney;
            System.out.println(this.getName()+"取了"+drawMoney+" : "+account.name+"餘額"+account.money);
        }
    }
}

wife取了100 : wedding fund餘額-50
you取了50 : wedding fund餘額-50
兩個人都操作了這100 所以總共提了150

e.g.3

public class List {
    public static void main(String args[]){
        ArrayList<String> list = new ArrayList<String>();
        for(int i=0;i<10000;i++){
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try{
            Thread.sleep(1000);
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

9998 --> 還是沒有全部thread都有 (不安全)

如何解決同步問題 - 1.synchronized

為控制一個對象或是方法,使用中的thread將其資源上鎖,使用完再釋放,
因此資源同一時間不會被多個thread給操作
以此保證程序運行的安全,但是同時也會降低效率

  1. sysynchronized 方法 public synchronized type method(int args){...}
  2. synchronized 區塊 synchronized(Obj){...}

一般來說,我們是針對被曾刪改的對象去把它鎖起來

--修改剛剛的範例--
e.g.1 鎖住方法

// 3 threads to controll same resource
public class Buyer {
    public static void main(String args[]){
        BuyTicket bt = new BuyTicket();

        new Thread(bt,"t1").start();
        new Thread(bt,"t2").start();
        new Thread(bt,"t3").start();
    }
}

class BuyTicket implements Runnable{
    private int t=10;
    boolean flag=true;

    public void run(){
        while(flag){
            buy();
        }
    }

    private synchronized void buy() {
        if(t<=0){
            flag=false;
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"拿到"+t--);
    }
}

e.g.2 鎖住物件

public class Bank {
    public static void main(String args[]){
        Account a = new Account(100, "wedding fund");

        Drawbank you = new Drawbank(a,50,"you");
        Drawbank wife = new Drawbank(a,100,"wife");

        you.start();
        wife.start();
    }
}

class Account{
    int money;
    String name;
    public Account(int m,String n){
        money=m;
        name=n;
    }
}

class Drawbank extends Thread{
    Account account;
    int drawMoney;

    public Drawbank(Account account,int drawMoney,String name){
        super(name);
        this.account=account;
        this.drawMoney=drawMoney;
    }

    public void run(){
        synchronized(account){
            if(account.money<drawMoney) System.out.println(this.getName()+"取錢時餘額不足");
            else{
                // sleep凸顯現實的問題
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.money=account.money-drawMoney;
                System.out.println(this.getName()+"取了"+drawMoney+" : "+account.name+"餘額"+account.money);
            }
        }
    }
}

you取了50 : wedding fund餘額50
wife取錢時餘額不足

e.g.3 鎖住物件

public class List {
    public static void main(String args[]){
        ArrayList<String> list = new ArrayList<String>();
        for(int i=0;i<10000;i++){
            new Thread(()->{
                synchronized(list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try{
            Thread.sleep(1000);
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

10000

如何解決同步問題 - 2.ReentrantLock

使用ReentrantLock創立的物件手動鎖定與釋放特定程式碼區域的資源

private final ReentrantLock lock = new ReentrantLock();
public void m(){
    lock.lock();
    try{
        //保證thread安全之程式碼
    }
    finally{
        lock.unlock();
    }
}

e.g.

public class Lock {
    public static void main(String args[]){
        BuyTicket bt =new BuyTicket();
        new Thread(bt).start();
    }
}

class Buytickets implements Runnable{
    int ticket = 10;
    private final ReentrantLock lock = new ReentrantLock();
    public void run(){
        while(true){
            try{
                lock.lock();
                if(ticket>0){
                    try{
                        Thread.sleep(1000);
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                    System.out.println(ticket--);
                }
                else break;
            }
            finally{
                lock.unlock();
            }
        }
    }
}

synchronized vs ReentrantLock

  • lock的鎖定與釋放都需要手動設定 syn則是出了程式快就自動釋放
  • lock只有區域鎖 syn有區域鎖跟方法鎖
  • lock 性能跟擴展性較佳
  • 在使用建議上: lock --> syn區域鎖 --> syn方法鎖

Deadlock

當thread握有別的thread想要的資源,卻又不放下自己手中的資源,此循環的資源請求情況,就形成了deadlock

e.g.

public class Deadlock{
    public static void main(String args[]){
        new Makeup(0, "Jassica").start();;
        new Makeup(1, "Susan").start();;
    }
}

class Lipstick{}
class Mirror{}

class Makeup extends Thread{

    //表示口紅及鏡子都只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int chioce;
    String girl;

    public Makeup(int choice,String name){
        this.chioce=choice;
        girl=name;
    }

    public void run(){
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void makeup() throws InterruptedException{
        if(chioce==0){ //先拿口紅再拿鏡子
            synchronized(lipstick){  //鎖住lipstick
                System.out.println(this.girl+" get lipstick!");
                Thread.sleep(1000);
                synchronized(mirror){ //鎖住lipstick又鎖住mirror
                    System.out.println(this.girl+" get mirror!");
                }
            }
        }
        else{
            synchronized(mirror){ //鎖住mirror
                System.out.println(this.girl+" get mirror!");
                Thread.sleep(1000);
                synchronized(lipstick){ //鎖住mirror又鎖住lipstick
                    System.out.println(this.girl+" get lipstick!");
                }
            }
        }
    }
}

Susan get mirror!
Jassica get lipstick!
Susan有了鏡子不放想要口紅
Jassica有了口紅不放想要鏡子 -->形成deadlock

解決方式: 持有資源a並且要請求資源b時,要釋放手中的資源a

public class Deadlock{
    public static void main(String args[]){
        new Makeup(0, "Jassica").start();;
        new Makeup(1, "Susan").start();;
    }
}

class Lipstick{}
class Mirror{}

class Makeup extends Thread{

    //表示口紅及鏡子都只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int chioce;
    String girl;

    public Makeup(int choice,String name){
        this.chioce=choice;
        girl=name;
    }

    public void run(){
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void makeup() throws InterruptedException{
        if(chioce==0){ //先拿口紅再拿鏡子
            synchronized(lipstick){  //鎖住lipstick
                System.out.println(this.girl+" get lipstick!");
                Thread.sleep(1000);
            }
            synchronized(mirror){ //放掉lipstick鎖住mirror
                System.out.println(this.girl+" get mirror!");
            }
        }
        else{
            synchronized(mirror){ //鎖住mirror
                System.out.println(this.girl+" get mirror!");
                Thread.sleep(1000);
            }
            synchronized(lipstick){ //放掉mirror鎖住lipstick
                System.out.println(this.girl+" get lipstick!");
            }
        }
    }
}

Jassica get lipstick!
Susan get mirror!
(頓一下)
Jassica get mirror!
Susan get lipstick!

生產者與消費者模型

code1 wait()/notifyAll()

public class PCproblem {
    public static void main(String args[]){
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }
}

class Productor extends Thread{
    SynContainer container;

    public Productor(SynContainer container){
        this.container=container;
    }

    public void run(){
        for(int i=0;i<100;i++){
            System.out.println("生產了"+i+"隻");
            container.push(new Chicken(i));
        }
    }
}

class Consumer extends Thread{
    SynContainer container;

    public Consumer(SynContainer container){
        this.container=container;
    }

    public void run(){
        for(int i=0;i<100;i++){
            System.out.println("消費了"+container.pop().id+"隻");
        }
    }
}

class Chicken{
    int id;
    public Chicken(int id){
        this.id=id;
    }
}

//容器
class SynContainer{
    Chicken[] chickens = new Chicken[10];
    int count=0;

    //生產者放入產品
    public synchronized void push(Chicken chicken){
        if(count==chickens.length){
            //通知消費者消費,生產者等待
            try{
                this.wait();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        chickens[count]=chicken;
        count++;

        //可以通知消費者了
        this.notifyAll();
    }

    //消費者拿出產品
    public synchronized Chicken pop(){
        if(count==0){
            //等待生產者生產
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;
        Chicken chicken =chickens[count];

        //通知生產者可以生產
        this.notifyAll();

        return chicken;
    }
}

code2 使用boolean flag

public class PCproblem2 {
    public static void main(String args[]){
        Stage stage = new Stage();
        Actor1 a1 = new Actor1(stage);
        Actor2 a2 = new Actor2(stage);

        a1.start(); a2.start();
    }
}

class Actor1 extends Thread{
    Stage stage = new Stage();
    public Actor1(Stage stage){
        this.stage=stage;
    }

    public void run(){
        for(int i=0;i<20;i++){
            this.stage.play1("綜藝大集合");
        }
    }
}

class Actor2 extends Thread{
    Stage stage = new Stage();
    public Actor2(Stage stage){
        this.stage=stage;
    }

    public void run(){
        for(int i=0;i<20;i++){
            this.stage.play2("快樂有go正");
        }
    }
}

//演員跟觀眾都會用到舞台 演員表演/觀眾觀賞
class Stage{
    // 演員1表演 演員2等待 T
    // 演員2觀看 演員1等待 F
    String program;
    boolean flag=true;

    //演員1
    public synchronized void play1(String program){
        if(!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("演員1表演了:"+program);
        //通知演員2表演
        this.notifyAll();
        this.program=program;
        this.flag=!this.flag;
    }

    //演員2
    public synchronized void play2(String program){
        if(flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("演員2表演了:"+program);
        //通知演員1表演
        this.notifyAll();
        this.program=program;
        this.flag=!this.flag;
    }
}

演員1表演了:綜藝大集合
演員2表演了:快樂有go正
演員1表演了:綜藝大集合
演員2表演了:快樂有go正
演員1表演了:綜藝大集合
演員2表演了:快樂有go正
演員1表演了:綜藝大集合
演員2表演了:快樂有go正
...

thread pool

public class Pool {
    public static void main(String args[]){

        //1.建立服務可以創建線程池 ,參數為pool中的線程數量
        ExecutorService service = Executors.newFixedThreadPool(10);

        service.execute(new thread());
        service.execute(new thread());
        service.execute(new thread());
        service.execute(new thread());

        //2.關閉服務連接
        service.shutdown();
    }
}

class thread implements Runnable{
    public void run(){
            System.out.println(Thread.currentThread().getName());
    }
}






你可能感興趣的文章

React 基礎:Props Drilling 和 useContext

React 基礎:Props Drilling 和 useContext

初試啼聲,只用原生 JS 跟 CSS 寫「口罩地圖 」Ep.01

初試啼聲,只用原生 JS 跟 CSS 寫「口罩地圖 」Ep.01

短網址系統設計

短網址系統設計






留言討論