博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
java多线程基础与编程——自己的理解通俗易懂
阅读量:4298 次
发布时间:2019-05-27

本文共 29425 字,大约阅读时间需要 98 分钟。

目录



1.几个基本的概念

    1.进程(Process):计算机软件程序比如QQ、lol这些程序都保存在磁盘上,用户点击运行,那么程序会被调用到内存上,cpu会一条一条的从内存上取指令,运行,这就是进程。

    2.线程:当用户执行程序时,会先将该程序加载到内存中,cpu从内存中取指令执行,进入cpu,还可以分多线程执行。

    3.进程的特点:进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。当用户同时执行两个程序时,即有两个进程,单核cpu会合理分配时间片,雨露均沾的快速切换执行,两个进程的地址空间是相互独立的,互不干扰。如果多核cpu,那么可以并行执行。

    4.进程的状态:

  • 新建:进程正在被创建
  • 运行:进程正在被执行
  • 阻塞:进程等待某个事件(如I/O操作)
  • 就绪:进程等待分配处理器
  • 终止:进程完成执行

    5.线程的状态

  • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  • 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他  线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的  使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  • 阻塞(BLOCKED):表示线程阻塞于锁。
  • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  • 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  • 终止(TERMINATED):表示该线程已经执行完毕。

    6.线程转换图(简化版)

 

源码分析图片

线程状态图

编程参考的图片

  7.线程与进程的关系

     进程是资源分配的最小单位,线程是程序执行的最小单位。

     线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

      多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。比如打开浏览器,开启多个窗口,是多线程还是多进程?因为一个页面卡住,不会影响其他页面,所以是多进程程序,开启新的页面就开启新的进程,如果用多线程来实现,页面之间相互影响,是不正确的。

     多线程一定是并发,多进程单核cpu并发,多进程多核cpu并行,多核cpu并行真的存在吗(不确定,有待进一步证明)

 8.单核cpu与多进程、多线程的效率问题

      单核cpu只能并发的执行多进程与多线程,至于效率问题,这里讨论多线程程序的效率问题,程序涉及到大量的阻塞问题,比如io阻塞,速度相对于cpu运算是很低的,导致该线程会阻塞等待一段时间,利用这个时间可以处理另一个线程,当io获取之后,返回执行之前的线程。

     深度好文推荐:

  9.同步锁——乐观锁与悲观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

 

2.创建线程的三种方法

Java中有两种创建线程的方式。

创建线程方式一:

  1. 继承Thread类(在java.lang包中),并重写该类的run()方法,其中run()方法即线程需要执行的任务代码。
  2. 然后new出这个类对象。这表示创建线程对象。
  3. 调用start()方法开启线程来执行任务(start()方法会调用run()以便执行任务)。

例如下面的代码中,在主线程main中创建了两个线程对象,先后并先后调用start()开启这两个线程,这两个线程会各自执行MyThread中的run()方法。

class MyThread extends Thread {    String name;    String gender;     MyThread(String name,String gender){        this.name = name;        this.gender = gender;    }     public void run(){        int i = 0;        while(i<=20) {            //除了主线程main,其余线程从0开始编号,currentThread()获取的是当前线程对象            System.out.println(Thread.currentThread().getName()+"-----"+i+"------"+name+"------"+gender);            i++;        }    }} public class CreateThread {    public static void main(String[] args) {        MyThread mt1 = new MyThread("malong","Male");        MyThread mt2 = new MyThread("Gaoxiao","Female");         mt1.start();        mt2.start();        System.out.println("main thread over");    }}

上面的代码执行时,有三个线程,首先是主线程main创建2个线程对象,并开启这两个线程任务,开启两个线程后主线程输出"main thread over",然后main线程结束。在开启两个线程任务后,这两个线程加入到了就绪队列等待CPU的调度执行。如下图。因为每个线程被cpu调度是随机的,执行时间也是随机的,所以即使mt1先开启任务,但mt2可能会比mt1线程先执行,也可能更先消亡。

 

创建线程方式二:

  1. 实现Runnable接口,并重写run()方法。
  2. 创建子类对象。
  3. 创建Thread对象来创建线程对象,并将实现了Runnable接口的对象作为参数传递给Thread()构造方法。
  4. 调用start()方法开启线程来执行run()中的任务。
class MyThread implements Runnable {    String name;    String gender;     MyThread(String name,String gender){        this.name = name;        this.gender = gender;    }     public void run(){        int i = 0;        while(i<=200) {            System.out.println(Thread.currentThread().getName()+"-----"+i);            i++;        }    }} public class CreateThread2 {    public static void main(String[] args) {        //创建子类对象        MyThread mt = new MyThread("malong","Male");        //创建线程对象        Thread th1 = new Thread(mt);        Thread th2 = new Thread(mt);         th1.start();        th2.start();        System.out.println("main thread over");    }}

这两种创建线程的方法,无疑第二种(实现Runnable接口)要好一些,因为第一种创建方法继承了Thread后就无法继承其他父类。

创建线程方式三:

new Thread(()->{                    while (true) {                        synchronized(o) {                                if (MySocketTestHandler.channelMap.containsKey("0")) {                                    Channel channel = MySocketTestHandler.channelMap.get("0");                                    System.out.println(channel.toString());                                    channel.writeAndFlush("发给柜子0的");                                    channel.closeFuture();                                }                        }                    }            }).start();

3.线程相关的常用方法

结合线程状态转换图来看:

Thread类中的方法:

  • currentThread():返回值为Thread,返回当前线程对象。
  • isAlive():判断线程是否还活着。活着的概念是指是否消亡了,对于运行态、就绪态、睡眠态的线程都是活着的状态。
  • getName():获取当前线程的线程名称。
  • setName():设置线程名称。给线程命名还可以使用构造方法Thread(String thread_name)Thread(Runnable r,String thread_name)
  • getPriority():获取线程优先级。优先级范围值为1-10(默认值为5),相邻值之间的差距对cpu调度的影响很小。一般使用3个字段MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY分别表示1、5、10三个优先级,这三个优先级可较大地区分cpu的调度。
  • setPriority():设置线程优先级。
  • run():封装的是线程开启后要执行的任务代码。如果run()中没有任何代码,则线程不做任何事情。
  • start():开启线程并让线程开始执行run()中的任务。
  • toString():返回线程的名称、优先级和线程组。
  • sleep(long millis):让线程睡眠多少毫秒。
  • join(t1):将线程t1合并到当前线程,并等待线程t1执行完毕后才继续执行当前线程。即让t1线程强制插队到当前线程的前面并等待t1完成。
  • yield():将当前正在执行的线程退让出去,以让就绪队列中的其他线程有更大的几率被cpu调度。即强制自己放弃cpu,并将自己放入就绪队列。由于自己也在就绪队列中,所以即使此刻自己放弃了cpu,下一次还是可能会立即被cpu选中调度。但毕竟给了机会给其它就绪态线程,所以其他就绪态线程被选中的几率要更大一些。

Object类中的方法:

  • wait():线程进入某个线程池中并进入睡眠态。等待notify()或notifyAll()的唤醒。
  • notify():从某个线程池中随机唤醒一个睡眠态的线程。
  • notifyAll():唤醒某个线程池中所有的睡眠态线程。

一般来说,wait()和唤醒的notify()或notifyAll()是成对出现的,否则很容易出现死锁。

那么sleep与wait有什么区别?

sleep是Thread类的方法,会让出CPU,不会导致锁行为的改变,可以在任何地方使用

wati是Object类的方法,只能在synchronized方法或synchronized块中使用,不仅让出CPU,还会释放已经占有的同步资源锁

下面看下代码来区分两者的区别

public class WaitSleepDemo_Sleep {    public static void main(String[] args){        Object lock = new Object();        new Thread(() ->{            System.out.println("Thread A 正在申请锁");            synchronized (lock) {                try {                    System.out.println("Thread A  拿到锁啦");                    Thread.sleep(1000);                    System.out.println("Thread A 结束");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();        try {            Thread.sleep(20);        } catch (InterruptedException e) {            e.printStackTrace();        }        new Thread(() ->{            System.out.println("Thread B 正在申请锁");            synchronized (lock) {                try {                    System.out.println("Thread B  拿到锁啦");                    Thread.sleep(1000);                    System.out.println("Thread B 结束");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();    }}

打印结果:

Thread A 正在申请锁

Thread A  拿到锁啦
Thread B 正在申请锁
Thread A 结束
Thread B  拿到锁啦
Thread B 结束
 

可以看到Thread.sleep不会释放锁,只有等A完成了B才拿到锁

public class WaitSleepDemo_Wait {    public static void main(String[] args){        Object lock = new Object();        new Thread(() ->{            System.out.println("Thread A 正在申请锁");            synchronized (lock) {                try {                    System.out.println("Thread A  拿到锁啦");                    lock.wait(1000);                    System.out.println("Thread A 结束");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();        try {            Thread.sleep(20);        } catch (InterruptedException e) {            e.printStackTrace();        }        new Thread(() ->{            System.out.println("Thread B 正在申请锁");            synchronized (lock) {                try {                    System.out.println("Thread B  拿到锁啦");                    lock.wait(1000);                    System.out.println("Thread B 结束");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();    }}

打印结果为:

Thread A 正在申请锁

Thread A  拿到锁啦
Thread B 正在申请锁
Thread B  拿到锁啦
Thread A 结束
Thread B 结束

可以看到Object.wait会释放锁,因为A还没有执行完,锁就被B获得了

java.util.concurrent.locks包中的类和它们的方法:

  • Lock类中:

    • lock():获取锁(互斥锁)。
    • unlock():释放锁。
    • newCondition():创建关联此lock对象的Condition对象。
  • Condition类中:

    • await():和wait()一样。
    • signal():和notify()一样。
    • signalAll():和notifyAll()一样。

4.多线程安全问题和线程同步

4.1 多线程安全问题

线程安全问题是指多线程同时执行时,对同一资源的并发操作会导致资源数据的混乱。

例如下面是用多个线程(窗口)售票的代码。

class Ticket implements Runnable {    private int num;    //票的数量     Ticket(int num){        this.num = num;    }     //售票    public void sale() {        if(num>0) {            num--;            System.out.println(Thread.currentThread().getName()+"-------"+remain());        }    }     //获取剩余票数    public int remain() {        return num;    }     public void run(){        while(true) {            sale();        }    }} public class ConcurrentDemo {    public static void main(String[] args) {        Ticket t = new Ticket(100);        //创建多个线程对象        Thread t1 = new Thread(t);        Thread t2 = new Thread(t);        Thread t3 = new Thread(t);        Thread t4 = new Thread(t);         //开启多个线程使其执行任务        t1.start();        t2.start();        t3.start();        t4.start();    }}

执行结果大致如下:

以上代码的执行过程大致如下图:(仔细分析!!!)

共开启了4个线程执行任务(不考虑main主线程),每一个线程都有4个任务:

  • ①判断if条件if(num>0);
  • ②票数自减num--;
  • ③获取剩余票数return num;
  • ④打印返回的num数量System.out.println(Thread.currentThread().getName()+"-------"+remain())

这四个任务的共同点也是关键点在于它们都操作同一个资源Ticket对象中的num,这是多线程出现安全问题的本质,也是分析多线程执行过程的切入点

当main线程开启t1-t4这4个线程时,它们首先进入就绪队列等待被CPU随机选中。(1).假如t1被先选中,分配的时间片执行到任务②就结束了,于是t1进入就绪队列等待被CPU随机选中,此时票数num自减后为99;(2).当t3被CPU选中时,t3所读取到的num也为99,假如t3分配到的时间片在执行到任务②也结束了,此时票数num自减后为98;(3).同理t2被选中执行到任务②结束后,num为97;(4).此时t3又被选中了,于是可以执行任务③,甚至是任务④,假设执行完任务④时间片才结束,于是t3的打印语句打印出来的num结果为97;(5).t1又被选中了,于是任务④打印出来的num也为97。

显然,上面的代码有几个问题:(1)有些票没有卖出去了但是没有记录;(2)有的票重复卖了。这就是线程安全问题。

4.2 线程同步

java中解决线程安全问题的方法是使用互斥锁,也可称之为"同步"。解决思路如下:

(1).为待执行的任务设定给定一把锁,拥有相同锁对象的线程在wait()时会进入同一个线程池睡眠。

(2).线程在执行这个设了锁的任务时,首先判断锁是否空闲(即锁处于释放状态),如果空闲则去持有这把锁,只有持有这把锁的线程才能执行这个任务。即使时间片到了,它也不是释放锁,只有wait()或线程结束时才会安全地释放锁。
(3).这样一来,锁被某个线程持有时,其他线程在锁判断后就继续会线程池睡眠去了(或就绪队列)。最终导致的结果是,(设计合理的情况下)某个线程一定完整地执行完一个任务,其他线程才有机会去持有锁并执行任务。

换句话说,使用同步线程,可以保证线程执行的任务具有原子性,只要某个同步任务开始执行了就一定执行结束,且不允许其他线程参与。

让线程同步的方式有两种,一种是使用synchronized(){}代码块,一种是使用synchronized关键字修饰待保证同步的方法。

class Ticket implements Runnable {    private int num;    //初始化票的数量    private Object obj = new Object();     Ticket(int num){        this.num = num;    }     //售票    public void sale() {        synchronized(obj) {   //使用同步代码块封装需要保证原子性的代码            if(num>0) {                num--;                System.out.println(Thread.currentThread().getName()+"-------"+remain());            }        }    }     //获取剩余票数    public int remain() {        return num;    }     public void run(){        while(true) {            sale();        }    }}
class Ticket implements Runnable {    private int num;    //初始化票的数量     Ticket(int num){        this.num = num;    }     public synchronized void sale() {  //使用synchronized关键字,方法变为同步方法        if(num>0) {            num--;            System.out.println(Thread.currentThread().getName()+"-------"+remain());        }    }     //获取剩余票数    public int remain() {        return num;    }     public void run(){        while(true) {            sale();        }    }}

使用同步之后,if(num>0)num--return numprint(num)这4个任务就强制具有原子性。某个线程只要开始执行了if语句,它就一定会继续执行直到执行完print(num),才算完成了一整个任务。只有完成了一整个任务,线程才会释放锁(当然,也可能继续判断while(true)并进入下一个循环)。总之个人理解——使用同步线程synchronized代码块后,整个代码块成为一个整体,看作一条指令,cpu只能一次性全部执行完synchronized代码块,然后被其他线程调用。

4.3 同步代码块和同步函数的区别以及锁是什么?

前面的示例中,同步代码块synchronized(obj){}中传递了一个obj的Object对象,这个obj可以是任意一个对象的引用,这些引用传递给代码块的作用是为了标识这个同步任务所属的锁。

使用相同的锁之间会互斥,但不同锁之间则没有任何影响。因此,要保证任务同步(原子性),这些任务所关联的锁必须相同。也因此,如果有多个同步任务(各自保证自己的同步性),就一定不能都使用同步函数。

例如下面的例子中,写了两个相同的sale()方法,并且使用了flag标记让不同线程能执行这两个同步任务。如果出现了多线程安全问题,则表明synchronized函数和同步代码块使用的是不同对象锁。如果将同步代码块中的对象改为this后不出现多线程安全问题,则表明同步函数使用的是this对象。如果为sale2()加上静态修饰static,则将obj替换为"Ticket.class"来测试。

class Ticket implements Runnable {    private int num;    //初始化票的数量    boolean flag = true;    private Object obj = new Object();     Ticket(int num){        this.num = num;    }     //售票    public void sale1() {        synchronized(obj) {  //使用的是obj标识锁            if(num>0) {                num--;                try{Thread.sleep(1);} catch (InterruptedException i){}  //为了确保num--和println()分开,加上sleep                System.out.println(Thread.currentThread().getName()+"===sale1==="+remain());            }        }    }     public synchronized void sale2() {   //使用this标识锁        if(num>0) {            num--;            try{Thread.sleep(1);} catch (InterruptedException i){}            System.out.println(Thread.currentThread().getName()+"===sale2==========="+remain());        }    }     //获取剩余票数    public int remain() {        return num;    }     public void run(){        if(flag){            while(true) {                sale1();            }        } else {            while(true) {                sale2();            }        }    }} public class Mytest {    public static void main(String[] args) {        Ticket t = new Ticket(200);        //创建多个线程对象        Thread t1 = new Thread(t);        Thread t2 = new Thread(t);         //开启多个线程使其执行任务        t1.start();        try{Thread.sleep(1);} catch (InterruptedException i){}        t.flag = false;        t2.start();    }}

以下是执行结果中的一小片段,出现了多线程安全问题。而如果将同步代码块中的obj改为this,则不会出现多线程安全问题。

  1. Thread-0===sale1===197

  2. Thread-1===sale2===========197

  3. Thread-0===sale1===195

  4. Thread-1===sale2===========195

  5. Thread-1===sale2===========193

  6. Thread-0===sale1===193

  7. Thread-0===sale1===191

  8. Thread-1===sale2===========191

 

5.Synchronized关键字用法详解

 

 一、修饰一个代码块。

        1.使用(this)。一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。我们看下面一个例子

/** * 同步线程 */class SyncThread implements Runnable {   private static int count;   public SyncThread() {      count = 0;   }   public  void run() {      synchronized(this) {         for (int i = 0; i < 5; i++) {            try {               System.out.println(Thread.currentThread().getName() + ":" + (count++));               Thread.sleep(100);            } catch (InterruptedException e) {               e.printStackTrace();            }         }      }   }   public int getCount() {      return count;   }}SyncThread syncThread = new SyncThread();Thread thread1 = new Thread(syncThread, "SyncThread1");Thread thread2 = new Thread(syncThread, "SyncThread2");thread1.start();thread2.start();

结果:

解释:当两个并发线程(thread1和thread2)访问同一个对象synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

我们改一下调用的主函数

结果如下:

解释:两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。

2.当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块

class Counter implements Runnable{   private int count;   public Counter() {      count = 0;   }   public void countAdd() {      synchronized(this) {         for (int i = 0; i < 5; i ++) {            try {               System.out.println(Thread.currentThread().getName() + ":" + (count++));               Thread.sleep(100);            } catch (InterruptedException e) {               e.printStackTrace();            }         }      }   }   //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized   public void printCount() {      for (int i = 0; i < 5; i ++) {         try {            System.out.println(Thread.currentThread().getName() + " count:" + count);            Thread.sleep(100);         } catch (InterruptedException e) {            e.printStackTrace();         }      }   }   public void run() {      String threadName = Thread.currentThread().getName();      if (threadName.equals("A")) {         countAdd();      } else if (threadName.equals("B")) {         printCount();      }   }//调用代码Counter counter = new Counter();Thread thread1 = new Thread(counter, "A");Thread thread2 = new Thread(counter, "B");thread1.start();thread2.start();}

结果:

解释:上面代码中countAdd是一个synchronized的,printCount是非synchronized的。从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。

二、修饰一个方法

Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){//todo}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。

public synchronized void run() {   for (int i = 0; i < 5; i ++) {      try {         System.out.println(Thread.currentThread().getName() + ":" + (count++));         Thread.sleep(100);      } catch (InterruptedException e) {         e.printStackTrace();      }   }}
public synchronized void method(){   // todo}

等效于下面代码

public void method(){   synchronized(this) {      // todo   }}

在用synchronized修饰方法时要注意:

synchronized关键字不能继承。

虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
在子类方法中加上synchronized关键字

class Parent {   public synchronized void method() { }}class Child extends Parent {   public synchronized void method() { }}

class Parent {   public synchronized void method() {   }}class Child extends Parent {   public void method() { super.method();   }}

三、修饰一个静态方法

我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。我们对Demo1进行一些修改如下:

/** * 同步线程 */class SyncThread implements Runnable {   private static int count;   public SyncThread() {      count = 0;   }   public synchronized static void method() {      for (int i = 0; i < 5; i ++) {         try {            System.out.println(Thread.currentThread().getName() + ":" + (count++));            Thread.sleep(100);         } catch (InterruptedException e) {            e.printStackTrace();         }      }   }   public synchronized void run() {      method();   }}//调用代码SyncThread syncThread1 = new SyncThread();SyncThread syncThread2 = new SyncThread();Thread thread1 = new Thread(syncThread1, "SyncThread1");Thread thread2 = new Thread(syncThread2, "SyncThread2");thread1.start();thread2.start();

结果:

syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。这与Demo1是不同的。

四、修饰一个类

Synchronized还可作用于一个类,用法如下:

class ClassName {   public void method() {      synchronized(ClassName.class) {         // todo      }   }}

代码:

/** * 同步线程 */class SyncThread implements Runnable {   private static int count;   public SyncThread() {      count = 0;   }   public static void method() {      synchronized(SyncThread.class) {         for (int i = 0; i < 5; i ++) {            try {               System.out.println(Thread.currentThread().getName() + ":" + (count++));               Thread.sleep(100);            } catch (InterruptedException e) {               e.printStackTrace();            }         }      }   }   public synchronized void run() {      method();   }}

结果:synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。

五、指定给某个对象枷锁

/** * 银行账户类 */class Account {   String name;   float amount;   public Account(String name, float amount) {      this.name = name;      this.amount = amount;   }   //存钱   public  void deposit(float amt) {      amount += amt;      try {         Thread.sleep(100);      } catch (InterruptedException e) {         e.printStackTrace();      }   }   //取钱   public  void withdraw(float amt) {      amount -= amt;      try {         Thread.sleep(100);      } catch (InterruptedException e) {         e.printStackTrace();      }   }   public float getBalance() {      return amount;   }}/** * 账户操作类 */class AccountOperator implements Runnable{   private Account account;   public AccountOperator(Account account) {      this.account = account;   }   public void run() {      synchronized (account) {         account.deposit(500);         account.withdraw(500);         System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());      }   }}//调用代码Account account = new Account("zhang san", 10000.0f);AccountOperator accountOperator = new AccountOperator(account);final int THREAD_NUM = 5;Thread threads[] = new Thread[THREAD_NUM];for (int i = 0; i < THREAD_NUM; i ++) {   threads[i] = new Thread(accountOperator, "Thread" + i);   threads[i].start();}

结果:

解释:

在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。

当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序。

public void method3(SomeObject obj){   //obj 锁定的对象   synchronized(obj)   {      // todo   }}

当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:

class Test implements Runnable{   private byte[] lock = new byte[0];  // 特殊的instance变量   public void method()   {      synchronized(lock) {         // todo 同步代码块      }   }   public void run() {   }}

说明:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

六、Wait/Notify通知者模式

通俗易懂的解释:怎么实现两个线程之间的通知事件,而非一个线程轮询两一个线程的耗费性能操作,可以使用Wait/Notify两个方法,

synchronized (Lock.class){}可以保证代码的原子性,但是如果代码中有等待另一个线程数据的的操作,需要暂时处于等待状态,把锁丢出去,找到另一个synchronized (Lock.class){},这是第二段代码可以获得锁,执行里面的代码,同时保证了代码的原子性,并且第二段代码处理得到了第一段代码想要的数据后,就把锁释放出去,等待第二段代码原子性执行完后,就执行第一段代码,原子性也执行结束。

附代码——idea多线程调试方便。

import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;public class ThreadTest {    static final Object obj = new Object();    private static boolean flag = false;    public static void main(String[] args) throws Exception {        Thread consume = new Thread(new Consume(), "Consume");        Thread produce = new Thread(new Produce(), "Produce");        consume.start();        Thread.sleep(1000);        produce.start();        try {            produce.join();            consume.join();        } catch (InterruptedException e) {            e.printStackTrace();        }    }    // 生产者线程    static class Produce implements Runnable {        @Override        public void run() {            synchronized (Lock.class) {                System.out.println("进入生产者线程");                System.out.println("生产");                try {                    TimeUnit.MILLISECONDS.sleep(2000);  //模拟生产过程                    flag = true;                    Lock.class.notify();  //通知消费者                    TimeUnit.MILLISECONDS.sleep(1000);  //模拟其他耗时操作                    System.out.println("退出生产者线程");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }    //消费者线程    static class Consume implements Runnable {        @Override        public void run() {            synchronized (Lock.class) {                System.out.println("进入消费者线程");                System.out.println("wait flag 1:" + flag);                    try {                        System.out.println("还没生产,进入等待");                        Lock.class.wait();                        System.out.println("结束等待");                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                System.out.println("wait flag 2:" + flag);                System.out.println("消费");                System.out.println("退出消费者线程");            }        }    }}

 

切换查看变量。

6.死锁(DeadLock)

最典型的死锁是僵局问题,A等B,B等A,谁都不释放,造成僵局,最后两个线程都无法执行下去。

例如下面的代码示例,sale1()中,obj锁需要持有this锁才能完成任务整体,而sale2()中,this锁需要持有obj锁才能完成任务整体。当两个线程都开始执行任务后,就开始产生死锁问题。

class Ticket implements Runnable {    private int num;        boolean flag = true;    private Object obj = new Object();     Ticket(int num){        this.num = num;    }      public void sale1() {        synchronized(obj) {   //obj锁            sale2();          //this锁        }    }     public synchronized void sale2() {   //this锁        synchronized(obj){               //obj锁            if(num>0) {                num--;                try{Thread.sleep(1);} catch (InterruptedException i){}                System.out.println(Thread.currentThread().getName()+"========="+remain());            }        }    }     //获取剩余票数    public int remain() {        return num;    }     public void run(){        if(flag){            while(true) {                sale1();            }        } else {            while(true) {                sale2();            }        }    }} public class DeadLockDemo {    public static void main(String[] args) {        Ticket t = new Ticket(200);        //创建多个线程对象        Thread t1 = new Thread(t);        Thread t2 = new Thread(t);         //开启多个线程使其执行任务        t1.start();        try{Thread.sleep(1);} catch (InterruptedException i){}        t.flag = false;        t2.start();    }}

为了避免死锁,尽量不要在同步中嵌套同步,因为这样很容易造成死锁。

7.编程思路

    1.重复单功能线程型问题。典型的问题是一个售票员,多个购票人。

class Ticket implements Runnable {    private int num;    //初始化票的数量    private Object obj = new Object();     Ticket(int num){        this.num = num;    }     //售票    public void sale() {        synchronized(obj) {   //使用同步代码块封装需要保证原子性的代码            if(num>0) {                num--;                System.out.println(Thread.currentThread().getName()+"-------"+remain());            }        }    }     //获取剩余票数    public int remain() {        return num;    }     public void run(){        while(true) {            sale();        }    }}

     2.多功能线程型问题。典型的问题是多个蛋糕店一起做蛋糕,多个人一起吃蛋糕。

写法1(不推荐):

public class ProducersAndConsumersDemo  {    public static void main(String[] args){         Cake cake = new Cake();         Object lock = new Object();        CakeConsumer cakeConsumer = new CakeConsumer(cake,lock);        CakeProducer cakeProducer = new CakeProducer(cake,lock);        Thread threadConsumer1 = new Thread(cakeConsumer,"小明");        Thread threadConsumer2 = new Thread(cakeConsumer,"小红");        Thread threadProducer1 = new Thread(cakeProducer,"商家AAA");        Thread threadProducer2 = new Thread(cakeProducer,"商家BBB");        threadConsumer1.start();        threadConsumer2.start();        threadProducer1.start();        threadProducer2.start();        while (Thread.currentThread().isAlive());        System.out.println("主线程结束!");    }}class CakeProducer implements Runnable {    Cake cake;    Object lock;    public CakeProducer(Cake cake,Object lock) {        this.cake = cake;        this.lock = lock;    }    public void producerCake() {        cake.makeCake();        System.out.println(Thread.currentThread().getName()+"生产了一个蛋糕");        System.out.println("蛋糕总数为"+cake.num);    }    @Override    public void run() {        while (true) {            try {                synchronized (lock) {                    producerCake();                }                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}class CakeConsumer implements Runnable  {    Cake cake;    Object lock;    public CakeConsumer(Cake cake,Object lock) {        this.cake = cake;        this.lock = lock;    }    public void consumerCake() {        if (haveCake()) {            cake.eatCake();            System.out.println(Thread.currentThread().getName()+"吃了一个蛋糕");            System.out.println("蛋糕还剩" + cake.num);        }    }    public boolean haveCake() {        if (cake.num == 0) {            System.out.println("没有蛋糕了,吃不了");            return false;        } else {            return true;        }    }    @Override    public void run() {        while (true) {            try {                synchronized (lock) {                    consumerCake();                }                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }} class Cake {     public Integer num;     public Cake() {         this.num = 0;     }     public void eatCake() {         num--;     }     public void makeCake() {         num++;     }}

打印结果:

没有蛋糕了,吃不了

商家BBB生产了一个蛋糕
蛋糕总数为1
商家AAA生产了一个蛋糕
蛋糕总数为2
小红吃了一个蛋糕
蛋糕还剩1
商家AAA生产了一个蛋糕
蛋糕总数为2
小红吃了一个蛋糕
蛋糕还剩1
小明吃了一个蛋糕
蛋糕还剩0

 

写法2(推荐):

public class ProducersAndConsumersDemo2 {    public static void main(String[] args){        Object lock = new Object();        Cake cake = new Cake();        ProducerCake producerCake = new ProducerCake(cake);        ConsumerCake cakeConsumer = new ConsumerCake(cake);        new Thread(() ->{            while (true) {                try {                    synchronized (lock) {                        producerCake.producerCake();                    }                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();        new Thread(() ->{            while (true) {                try {                    synchronized (lock) {                        cakeConsumer.consumerCake();                    }                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();    }}class ProducerCake  {    Cake cake;    public ProducerCake(Cake cake) {        this.cake = cake;    }    public void producerCake() {        cake.makeCake();        System.out.println(Thread.currentThread().getName()+"生产了一个蛋糕");        System.out.println("蛋糕总数为"+cake.num);    }} class ConsumerCake {    Cake cake;    public ConsumerCake(Cake cake) {        this.cake = cake;    }    public void consumerCake() {        if (haveCake()) {            cake.eatCake();            System.out.println(Thread.currentThread().getName()+"吃了一个蛋糕");            System.out.println("蛋糕还剩" + cake.num);        }    }    public boolean haveCake() {        if (cake.num == 0) {            System.out.println("没有蛋糕了,吃不了");            return false;        } else {            return true;        }    }}

打印结果:

Thread-0生产了一个蛋糕

蛋糕总数为1
Thread-1吃了一个蛋糕
蛋糕还剩0
Thread-0生产了一个蛋糕
蛋糕总数为1
Thread-1吃了一个蛋糕
蛋糕还剩0
Thread-0生产了一个蛋糕
蛋糕总数为1

 

 

 

你可能感兴趣的文章
C指针声明解读之左右法则
查看>>
一个异步网络请求的坑:关于NSURLConnection和NSRunLoopCommonModes
查看>>
iOS 如何放大按钮点击热区
查看>>
ios设备唯一标识获取策略
查看>>
获取推送通知的DeviceToken
查看>>
Could not find a storyboard named 'Main' in bundle NSBundle
查看>>
CocoaPods安装和使用教程
查看>>
Beginning Auto Layout Tutorial
查看>>
block使用小结、在arc中使用block、如何防止循环引用
查看>>
iPhone开发学习笔记002——Xib设计UITableViewCell然后动态加载
查看>>
iOS开发中遇到的问题整理 (一)
查看>>
Swift code into Object-C 出现 ***-swift have not found this file 的问题
查看>>
为什么你的App介绍写得像一坨翔?
查看>>
RTImageAssets插件--@3x可自动生成@2x图片
查看>>
iOS开发的一些奇巧淫技
查看>>
常浏览的博客和网站
查看>>
Xcode 工程文件打开不出来, cannot be opened because the project file cannot be parsed.
查看>>
点击button实现Storyboard中TabBar Controller的tab切换
查看>>
Xcode 的正确打开方式——Debugging
查看>>
打包app出现的一个问题
查看>>