• 多线程好文:添加链接描述
  • 锁机制:synchronized、Lock、Condition、volatile(原子性可见性)——参考添加链接描述
    1、进程与线程概述
      首先,对于CPU执行每一个程序,其实都是在快速切换程序,一个核的CPU只能同时执行一个任务,因为CPU切换很快,我们反应不过来。
      所谓进程,字面意思上理解就是“是一个正在执行中的程序”。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。对于一个进程,它还有可能分为多条执行路径(如进程迅雷下载可能分为多路径进行下载,这多条路径同时发送下载请求到服务端,这样就可以多路径同时下载,效率更高),这每一条执行路径就称为线程。
      也就是说,线程是进程中的内容,每一个应用程序(进程)至少有一个线程,因为线程是进程独立的控制单元,或者说是执行路径,线程在控制着进程的执行。(结合视频11-1,10分30秒开始处对进程线程进行理解)
      对于java,它有一个编译进程和一个运行进程。Java 虚拟机启动的时候会有一个进程java.exe,该进程中至少一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程。其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。(一个进程中有多个线程在执行,这就是多线程)。
    关于进程与线程的深入理解,参考如下文章:
    深入理解进程线程

2、线程的创建
  我们写代码的时候也想自定义创建一些线程,让某些代码可以同时执行。
  线程对象是可以产生线程的对象。比如在Java平台中Thread对象,Runnable对象。线程,是指正在执行的一个指点令序列。在java平台上是指从一个线程对象的start()开始,运行run方法体中的那一段相对独立的过程。相比于多进程,多线程的优势有:

  1. 进程之间不能共享数据,线程可以;
  2. 系统创建进程需要为该进程重新分配系统资源,故创建线程代价比较小;
  3. Java语言内置了多线程功能支持,简化了java多线程编程。
      如何在自定义的代码中,自定义一个线程呢?java已经提供了对线程这类事物的描述,就是Thread类。

2.1、继承Thread类创建线程类
  通过继承Thread类创建线程类的具体步骤和具体代码如下:
1)定义一个继承Thread类的子类,并重写该类的run()方法;
  目的:将自定义代码存储在run方法,让线程运行。

2)创建Thread子类的实例,即创建了线程对象;

3)调用该线程对象的start()方法启动线程。
  该方法两个作用:启动线程,调用run方法。

class Demo extends Thread
{public void run(){System.out.println("thread run");}
}public class ThreadDemo
{public static void main(String[] args) {Demo d = new Demo();//建立好一个继承Thread类的类Demo的对象,相当于创建好一个线程d.start();//start():使该线程开始执行,Java 虚拟机调用该线程的 run 方法。
//      结果是两个线程并发地运行,当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。 d.run();//直接写d.run(),这就相当于没有开启另一个线程,而是直接调用的run()方法,就是直接执行完run(),再执行下面代码,只有一个主线程。Thread t = new Thread();//这样也相当于创建一个线程t.start();//开启线程,start()调用run()非法//但是这样我们不知道要运行什么代码,因为run()非法没有重写,没有运行内容//而且开启线程是为了运行自己制定的代码,上面这样设置线程没有意义}
}
//结果是:thread run

  再看下面这个例子,我们发现2个线程(主线程以及Demo线程)是穿插打印的。(参考视频11-2的11分30秒开始处的解释)这个结果说明2个线程在同时执行。(一核CPU在一个时候只能执行一个进程,看起来是在同时执行多个进程,其实CPU是在切换进程,更细一点说,CPU是在切换线程)如果CPU有多核,就可以达到同时执行的效果。

class Demo extends Thread
{public void run(){for(int x=0 ; x<60 ; x++)System.out.println("thread run"+x);}
}public class ThreadDemo
{public static void main(String[] args) {Demo d = new Demo();d.start();for(int x=0 ; x<60 ; x++)System.out.println("demo run"+x);}
}


  发现运行结果每一次都不同。因为多个线程都获取cpu的执行权,cpu执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行(多核除外),cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。
  这就是多线程的一个特性:随机性,谁抢到谁执行,至于执行多长,cpu说的算。
  为什么要覆盖run方法呢?Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。

  练习:

class Demo extends Thread
{private String name;Demo(String name){this.name = name;}public void run(){for(int x=0 ; x<60 ; x++)System.out.println(name+"运行"+x);}
}public class ThreadDemo
{public static void main(String[] args) {//注意,不需要创建多个Demo继承Thread,因为只要new一个Demo的对象就是创建一个新的线程Demo d1 = new Demo("线程一");Demo d2 = new Demo("线程二");d1.start();d2.start();for(int x=0 ; x<60 ; x++)System.out.println("主线程运行"+x);  }
}

  接下来说一下线程运行状态,线程运行有4种常见状态(当然还有其他状态,如阻塞):被创建,运行,冻结,消亡,这4种状态之间的关系如下图:(参考视频11-5的解释)

  需要说明的是,运行状态具有运行资格,也有执行权,当CPU挂起这个线程,进入阻塞状态,阻塞状态具有运行资格(可以运行),但是没有执行权(CPU执行它),当CPU又进入这个线程使其执行,又会从阻塞状态回到运行状态。
  另一方面,运行状态可以通过sleep(time)或者wait()进入冻结状态,sleep(时间)到就会返回,而wait()需要notify()方法使其返回。冻结状态放弃了运行资格(CPU的执行权在这个线程手上,是这个线程自己不想执行,放弃资格,进入冻结状态的时候他也有执行权,因为CPU在这个线程)。冻结状态返回的时候会先返回阻塞状态(有运行资格,没有CPU执行权),因为它返回的时候又取回自己的运行资格,但是它冻结返回时,不知道CPU还在不在这个线程执行。所以先回阻塞,如果CPU在这个线程执行,就恢复执行权返回运行状态;如果CPU不在这个线程,就现在阻塞状态等待CPU回来给他赋予执行权。

线程的五大状态及其转换
1、resume与suspended一起使用, wait与notify(notifyAll)一起使用, sleep会让线程暂时不执行 。   suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。2、线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。1)新建状态(New): 当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码2)就绪状态(Runnable)一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。3)运行状态(Running)当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.4)阻塞状态(Blocked)线程运行过程中,可能由于各种原因进入阻塞状态: 1>线程通过调用sleep方法进入睡眠状态; 2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者; 3>线程试图得到一个锁,而该锁正被其他线程持有; 4>线程在等待某个触发条件; ......所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间, 进入运行状态。5)死亡状态(Dead)有两个原因会导致线程死亡:i) run方法正常退出而自然死亡,ii) 一个未捕获的异常终止了run方法而使线程猝死。 为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是 可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了, 则返回false.

  当我们创建的线程一多,我们就需要一些标识来区分线程,线程也有自己的名称。想获取线程名称,我们就得获取线程的对象。
  示例1:获取线程名称

/*
线程都有自己默认的名称,格式为:Thread-编号,该编号从0开始。
*/
class Demo extends Thread
{private String name;Demo(String name){this.name = name;}public void run(){for(int x=0 ; x<60 ; x++)//此处用this代表Demo类的对象调用Demo父类Thread的getName()方法来获取线程名称System.out.println(this.getName()+"运行"+x);}
}public class ThreadDemo
{public static void main(String[] args) {Demo d1 = new Demo("线程一");Demo d2 = new Demo("线程二");d1.start();d2.start();for(int x=0 ; x<60 ; x++)System.out.println("主线程运行"+x);   }
}

  示例2:设置取线程名称

/*
此处不用setName()方法设置线程名称,Thread类有一个Thread(String name)的构造方法,因此我们在初始化的时候就可以设置线程名称。
我们可以通过Thread类的有参构造方法,直接给线程名称赋值,而不需要通过setName()方法
也就得说,只要我们在子类中调用父类Thread的有参数的构造方法,我们就可以直接给线程对象赋值
thread类中有类似如下代码。
class Thread
{private String name;//这是存储线程名称的变量Thread(String name){this.name = name;//通过Thread有参构造方法给线程名称赋值}
}
*/
class Demo extends Thread
{Demo(String name){super(name);//调用父类Thread类有参的构造方法,并将想设置的名称用name参数传入}public void run(){//对于运行代码,每一个线程都有独立的一份,就是每一个线程都会有自己的内存放这些运行代码//如每一个线程都有自己的内存空间放自己的run()方法,局部的变量在每一个线程内存区域都有独立的一份for(int x=0 ; x<60 ; x++)System.out.println(Thread.currentThread().getName()+"运行"+x);//这里也可以是this.getName(),使用子类的引用(一个引用创建一个线程)调用父类的方法,this代表下面的d1、d2//currentThread(),该方法是静态方法,作用是:返回对当前正在执行的线程对象的引用,这里也是子类的引用(一个引用创建一个线程)调用父类的方法//this == Thread.currentThread(),但是Thread.currentThread()是通用的,而this不是通用的,这涉及到第二种线程创建方式}
}public class ThreadDemo
{public static void main(String[] args) {Demo d1 = new Demo("one---");Demo d2 = new Demo("two+++");d1.start();d2.start();for(int x=0 ; x<60 ; x++)System.out.println("主线程运行"+x);  }
}

  总结:

  1. Thread.currentThread():获取当前线程对象;
  2. getName(): 获取线程名称;
  3. 设置线程名称:setName或者构造函数。

2.2、实现Runnable接口创建线程类
  我们通过2个例子来说明这种创建方式,例子1:其他2类方法

/*
需求:简单的卖票程序,多个窗口同时买票。
*///创建一个卖票类,里面的卖票步骤要多线程执行,那么里面必须实现多线程的run()方法,将需要多线程执行的代码放到run()里面
package pack;
class Ticket extends Thread
{private static int ticket = 100;//用Ticket的构造方法设置线程对象的名称,如果我们不为线程设定名称,就会使用默认线程代号Ticket(String name){super(name);}public void run(){while(true){if(ticket > 0){System.out.println(Thread.currentThread().getName()+"剩余票数为:"+ticket--);}}}
}public class ThreadDemo
{public static void main(String[] args) {Ticket t1 = new Ticket("窗口一");
//      Ticket t2 = new Ticket("窗口二");
//      Ticket t3 = new Ticket("窗口三");
//      Ticket t4 = new Ticket("窗口四");t1.start();t1.start();t1.start();t1.start();}
}
/*
结果我们发现每一个窗口都卖了100张票,而我们实际上只有100张票,这与实际是不符的。问题就在于,每创建一个对象里面就有100张票。
解决方案:让4个对象共享100张票 方法1:将ticket设置为静态。
对于普通变量,是在对象加载的时候才会进入内存,因此当t1、t2、t3、t4分别调用start()方法,run()方法分别进入内存,
这时就会分别加载4个ticket进入内存,而且每一个ticke都是从100开始。如果我们设置ticket为静态,那么它在最开始Ticket类进入内存的时候,
就会赋值100加载进入内存,这个时候,不管后面多少个对象调用它,都只有一个静态的ticket。
(都是,由于静态成员的生命周期太长,我们不将ticket设置为静态)方法2:只设置一个Ticket对象,一个对象多次启动线程,但是这种情况下会报错:无效的线程Exception in thread "main":java.lang.IllegalThreadStateExceptionat java.lang.Thread.start(Thread.java:708)at pack.ThreadDemo.main(ThreadDemo.java:39)
线程已经从创建状态到运行状态,再次开启线程就没有意义。*/

  例子2:实际使用方法

//创建一个卖票类,里面的卖票步骤要多线程执行,那么里面必须实现多线程的run()方法,将需要多线程执行的代码放到run()里面
package pack;
class Ticket implements Runnable
{private int ticket = 100;public void run(){//不可以将ticket设置在run()方法之内,这样每一个线程进来调用run()方法都会重新有一个ticket,还是原来的4个线程的ticket都是从100开始
//而将ticket放在外面,我们只创建了一个Ticket对象,因此它只指向一个ticket成员,这样其他线程进来,都只有一个ticket;而前一种方法如果要有4个线程就要创建4个Ticket 对象,这样就会有4个ticket从100开始
//那么所有的线程都会使用一个ticket
//      int ticket = 100;while(true){if(ticket > 0){System.out.println(Thread.currentThread().getName()+"剩余票数为:"+ticket--);}}}
}public class ThreadDemo
{public static void main(String[] args) {//创建一个Ticket类的对象,这个对象不是线程,它与Thread类没有关系。只有Thread类及其子类对象可以创建线程Ticket t = new Ticket();//Thread有一个可以接受Runnable接口类型对象的构造方法:public Thread(Runnable target)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个线程同时在售100张票*/

  方法2创建线程的步骤:

  1. 定义类实现Runnable接口;
  2. 覆盖Runnable接口中的run方法,将线程要运行的代码存放在该run方法中;
  3. 创建实现Runnable接口的子类对象;
  4. 通过Thread类建立线程对象,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数;
  5. 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。

  为什么要将Runnable接口的子类对象传递给Thread的构造函数?
  因为,自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去执行指定对象的run方法,就必须明确该run方法所属对象。

  关键:实现方式和继承方式有什么区别呢?实现方式好处:避免了单继承的局限性,因为如果我们继承了其他类,就无法继承Thread类实现多线程。(从视频11-8的13分钟开始处的解释)在定义线程时,建议使用实现方式。
  在本例子中还有一个好处,我们发现继承的方法,创建多个线程就有多个100张的票,每一个线程都有自己的run()方法;而在实现方法里面,我们创建Ticket的对象再将这个对象赋予每一个线程,这些线程实际上都是在共享同一个ticket,因为只创建一个Ticket对象,一个Ticket对象指向一个ticket,这样,就将“票”资源独立出来,分给多个线程共享。而继承方法没办法共享“票”资源!
  (Runnable接口的定义,其实就是在确立线程要运行代码所存放的位置,Thread类也实现Runnable接口)

  两种方式区别:
1)继承Thread:线程代码存放Thread子类run方法中;
2)实现Runnable,线程代码存在接口的子类的run方法。

3、多线程安全问题与同步代码块
3.1、线程安全问题
   注意视频11-9开始处分析的可能出现的错误。通过分析,我们发现多线程在运行的时候可能打印出0,-1,-2等错票,多线程的运行出现了安全问题。

/*
通过分析,我们发现多线程在运行的时候可能打印出0,-1,-2等错票
*/package pack;
class Ticket implements Runnable
{private int ticket = 100;public void run(){while(true){if(ticket > 0){//我们使用Thread类的public static void sleep(long millis)throws InterruptedException方法来模拟视频中线程可能出现的错误//也就是使用sleep()方法让线程暂停一段时间,该方法如果被interrupt()方法中断,会抛出InterruptedException异常。//由于run()方法是重写接口的方法,它里面抛出的异常必须在run()里面处理,不能抛出。(注意接口的方法无法抛出异常)try{//调用thread的sleep方法,停10毫秒,模拟线程失去CPU执行权挂起Thread.sleep(5);}catch(Exception e){//不处理}System.out.println(Thread.currentThread().getName()+"....sale::"+ticket--);}}}
}public class ThreadDemo
{public static void main(String[] args) {Ticket t = new Ticket();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();}
}

   运行如上代码我们发现出现了视频中描述地错误——有负数的票在售卖!

   问题的原因: 在多线程中,当有多条语句在操作同一个线程共享数据(如此处的ticket)时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
   解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,其他线程才可以执行。在一个线程的执行过程中,其他线程不可以参与执行。
   Java对于多线程的安全问题提供了专业的解决方式,就是同步代码块。格式如下:

synchronized(对象)
{需要被同步的代码
}
//这里的对象用什么类的对象都可以,我们一般使用Object类的对象

   对象如同锁,持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。相应的代码如下:

class Ticket implements Runnable
{private int ticket = 100;Object obj = new Object();//run()方法里面的代码全部是线程运行代码,只有那些操作了多线程共享数据的代码需要被同步public void run(){while(true){synchronized (obj)//同步{if(ticket > 0){try{Thread.sleep(5);//调用thread的sleep方法,停10毫秒}catch(Exception e){//不处理}System.out.println(Thread.currentThread().getName()+"....sale::"+ticket--);}}}}
}public class ThreadDemo
{public static void main(String[] args) {Ticket t = new Ticket();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();}
}
/*结果发现不再出现0,-1,-2的情况*/

3.2、多线程同步代码块synchronized
3.2.1、synchronized运行原理
   对于上一节例子中的同步代码块——synchronized的运行原理解释:(具体参考视频11-10开始处解释)

while(true){synchronized(obj)//obj就是“同步锁”,专业说法叫做监视器!{if(tick>0){//try{Thread.sleep(10);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);}}}

   对象如同锁,持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
   同步的前提:

  1. 必须要有两个或者两个以上的线程;
  2. 必须是多个线程使用同一个锁。(如果多个线程使用的是不同的代码,也必须要使用同一个锁。)

  必须保证同步中只能有一个线程在运行。且只有那些操作了多线程共享数据的代码需要被同步。

  好处:解决了多线程的安全问题;弊端:多个线程需要判断锁,较为消耗资源。

3.2.2、多线程同步函数与如何选择需要同步的代码
  我们如何选择需要同步的代码呢?我们看如下例子

/*
需求:银行有一个金库,有两个储户分别存300元,每次存100,存3次。注意金库总钱数sum是2个储户共同操作的目的:该程序是否有安全问题,如果有,如何解决?如何找问题:
1)明确哪些代码是多线程运行代码;(run()方法里面都是多线程运行代码)
2)明确共享数据;(这里b与sum都是共享数据!注意b也是!因为只有一个金库,那么只有一个金库对象!)
3)明确多线程运行代码中哪些语句是操作共享数据的(有多条代码操作共享数据的时候才需要进行同步)。同步2种表现形式:同步代码块与同步函数。当二者封装的代码相同时,使用同步函数较方便!
*/package pack;//创建一个银行类
class Bank
{private int sum;//定义一个金库存储总钱数的变量sum,这个变量是2个储户共同操作的变量
//  Object obj = new Object();将同步关键字用来修饰函数,不需要对象!//我们注意到,同步代码块是用来封装代码,而函数也是用来封装代码。2者唯一的区别是同步代码块带有同步的特性,而函数没有,而他们封装的代码是一样的。//如果我们使得函数有了同步的特性,就不需要同步代码块了!public synchronized void add(int n)//将同步关键字用来修饰函数{//      synchronized(obj)//添加同步后输出正常,可以打印每一次的添加过程
//      {sum = sum + n;//假设线程0在这里挂起,还没有打印,接下来线程1进来又将sum+100,此时线程0苏醒,执行打印//那么线程0打印出来的sum只有200而没有100这条添加的记录。接下来线程1苏醒,线程0挂起,又打印一次存储200的记录 try{Thread.sleep(10);}catch(Exception e) {};System.out.println(Thread.currentThread().getName()+"金库存钱总数sum="+sum);
//      }}
}//创建一个储户类,该类要实现多储户存钱的动作,有多个线程,实现Runnable接口
class Customer implements Runnable
{private Bank b = new Bank();//创建一个私有的金库对象public void run(){//可以在for的地方就添加同步,但是这样的话只能一个人添加完全部的300后,另一个人才能添加。注意哪些代码要同步,哪些代码不应该同步!for(int x=0; x<3 ;x++)//x不需要同步,因为每一个线程有自己的x,它不是共享数据{b.add(100);//添加钱,只有一句代码操作共享数据,不需要同步}}
}public class ThreadDemo
{public static void main(String[] args) {Customer c = new Customer();Thread obj1 = new Thread(c);Thread obj2 = new Thread(c);obj1.start();obj2.start();}
}
/*错误结果:
Thread-1金库存钱总数sum=200
Thread-0金库存钱总数sum=200
Thread-0金库存钱总数sum=400
Thread-1金库存钱总数sum=400
Thread-0金库存钱总数sum=600
Thread-1金库存钱总数sum=600
*/

3.2.3、同步函数锁的原理
  我们再看3.2.1中卖票的例子。

/*同步函数用的是哪一个锁呢?
函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this。(注意同步的锁是对象)如果要同步的代码块有循环就不要用同步函数。*/package pack;
class Ticket implements Runnable
{private int ticket = 100;
//  Object obj = new Object();public void run(){//不可以直接将synchronized放在run()方法处,因为一当某一个线程进去,里面是while(true)的无线循环,//这样这个线程一直不会结束,那么其他线程一直进不来,导致只有一个线程在卖票,而且这个线程卖完票,也不会结束。while(true){//          synchronized (obj){}this.show();//此处的this相当于Ticket类的对象}}//我们可以将需要同步的代码单独封装为一个方法,并使得这个方法同步即可public synchronized void show(){if(ticket > 0){try{Thread.sleep(5);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....sale::"+ticket--);}    }
}public class ThreadDemo
{public static void main(String[] args) {Ticket t = new Ticket();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();}
}

  我们通过下面的例子进行验证:

/*同步函数用的是哪一个锁呢?
函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this。(注意同步的锁是对象)我们通过如下程序来验证:使用两个线程来卖票,一个线程在同步代码块中,一个线程在同步函数中,都在执行卖票动作。
如果2个线程是同步的,那么他们就不会出现错误的票。*/package pack;
class Ticket implements Runnable
{private int ticket = 100;Object obj = new Object();boolean flag = true;//设置一个布尔型的变量来控制2个线程的切换public void run(){if(flag)//一个线程设置为真,运行同步代码块{while(true){synchronized(this){if(ticket > 0){try{Thread.sleep(5);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....同步代码块::"+ticket--);} }}}else//一个线程设置为假,运行同步函数{while(true){show();}}}public synchronized void show(){if(ticket > 0){try{Thread.sleep(5);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....同步函数::"+ticket--);}    }
}public class ThreadDemo
{public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();//线程1进去为真,运行同步代码块。但是运行到这里这个线程不一定立即执行,可能CPU在主线程那边,所以这一句可能不会立即执行!try{Thread.sleep(10);}catch(Exception e){}//使得主线程停一下,在这段时间内如果t1获得CPU的执行权,就会执行同步代码块t.flag = false;//一个线程运行结束,将flag设置为false,另一个线程进去运行同步函数t2.start();System.out.println("over");}
}
/*
结果1:
发现,线程0和线程1都在同步函数里面执行,而不在同步代码块里面执行。
一共有3个线程,因为主线程可能执行太快了,将
t1.start();
t.flag = false;
t2.start();
3句代码全部执行完,再执行其他2个线程,这个时候flag为false,那么只在同步函数里面执行!
因此我们必须加一个sleep来延时,防止主线程执行太快!
*//*
结果2:
Thread-0....同步代码块::2
Thread-1....同步函数::1
Thread-0....同步代码块::0发现出现序号为0的票,这样操作不安全!
为什么?同步2个前提:多个线程,线程使用同一个锁。
因此我们将同步代码块的锁改为this。发现错误消失了!这说明同步函数的锁是this!对于:也就是将flag置为false之后,运行线程1同步函数之后,还会再运行flag=true时的同步代码块。而且线程0只对应同步代码块,线程1只对应同步函数。这是因为我们设置sleep之后,线程0会获得CPU执行权进去,这个时候flag=true,它会执行同步代码块,而下次线程0再获得CPU执行权,这个时候它还是会从flag=true时的状态进入run()方法,还是执行同步代码块。如果主函数不sleep,那么线程0第一次开启flag=false,下次再进来还是会从flag=false的状态进入run()。总结:线程再次获取CPU执行权运行多线程代码的时候,还是会回到它第一次进入多线程代码时的状态——如这里线程0再次运行会回到flag=false之前进行多线程代码。
Thread-1....同步函数::20
Thread-1....同步函数::19
Thread-0....同步代码块::18
Thread-0....同步代码块::17
*/

3.2.4、静态同步函数的锁
  我们通过下面的例子来说明静态同步函数的锁

/*如果同步函数被静态修饰后,使用的锁是什么呢?通过验证,发现不在是this,因为静态方法中也不可以定义this!静态进内存,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class  该对象的类型是Class字节码文件对象既Class 类名.class = new Class();“.class”是字节码文件,类名.class就是类的字节码文件对象静态的同步方法,使用的锁是该方法所在类的字节码文件对象:类名.class,并且由于字节码文件唯一,那么该字节码文件对象在内存里面是唯一的。*/package pack;
class Ticket implements Runnable
{private static int ticket = 100;//同步函数使用的成员变量也设置为静态Object obj = new Object();boolean flag = true;//设置一个布尔型的变量来控制2个线程的切换public void run(){if(flag){while(true){synchronized(Ticket.class){if(ticket > 0){try{Thread.sleep(5);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....同步代码块::"+ticket--);}    }}}else{while(true){show();}}}public static synchronized void show()//将同步函数设置为静态{if(ticket > 0){try{Thread.sleep(5);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....同步函数::"+ticket--);} }
}public class ThreadDemo
{public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();try{Thread.sleep(10);}catch(Exception e){}t.flag = false;t2.start();}
}
/*
结果1:
......
Thread-1....同步函数::2
Thread-0....同步代码块::1
Thread-1....同步函数::0我们发现出现为0的票,错误!说明静态代码块与静态方法用的不是同一个锁,说明静态方法用的锁不是this
将静态代码块的对象改为Ticket.class之后,没有0的票,说明静态代码块与静态方法使用的对象相同,都是ticket.class
*/

3.3、多线程的单例设计模式
  下面例子中我们讨论一下多线程模式下的单例设计模式:

package pack;
/*饿汉式(一般写饿汉式没问题,所以写饿汉式!)
class Single
{//私有化构造方法private Single() {}//创建一个私有的对象private static final Single s = new Single();//设计一个方法将私有的对象提供出去public static Single getInstance(){return s;}
}
*///懒汉式
class Single
{private Single() {}private static Single s = null;//s是共享数据,可能有多个线程并发访问getInstance()方法。//我们可以在函数处设置同步函数,但是如果线程很多,每一个线程想获取对象,都必须判断函数的锁,这样程序比较低效public static Single getInstance(){//我们使用同步代码块来做,但是这样仍然是每一个对象进来都要判断锁,那么我们再加一个判断条件//这样一个线程进来,将s设置为对象,以后进来判断s不为空而是new Single(),就不需要再判断锁,直接返回!(视频11-14,6.32秒处开始的解释)//这样的话就不需要每一个线程都来判断锁(同步函数每一个线程都要判断锁),只有前面2个线程需要判断(视频解释),而且多个线程只能创建一个对象!//这种叫做用双重判断的方式解决问题!减少判断锁的次数,提高了懒汉式的效率。if(s == null){synchronized(Single.class)//我们使用Single类的字节码文件对象作为synchronized的对象锁{//这里有多条语句在操作共享数据s,一条在判断,一条在赋值if(s == null)//-->A:线程A挂在这里 -->B:线程B挂在这里//当线程A苏醒,创建一个对象,线程B苏醒,又创建一个对象。本来就应该只有一个对象!//因此我们将操作共享数据的代码同步起来s = new Single();}}return s;}
}/*面试:懒汉式与饿汉式有什么不同?
懒汉式的特点在于延迟实例加载,但是在多线程访问时,懒汉式容易出现安全问题。可以加同步来解决这个问题,同步代码块与同步函数都可以,
但是同步函数有些低效,而同步代码块也是,用双重判断的形式可以解决同步代码块的低效问题。使用的锁是Single类所属的字节码文件对象:Single.class
*/

3.4、多线程死锁
1)死锁产生原因?(见视频11-15解释)
  Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。
  从这两个例子,我们可以得出结论,产生死锁可能性的最根本原因是:线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,也就是说在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。简单地讲,就是同步中嵌套同步,而锁却不同。另一个原因是默认的锁申请操作是阻塞的。
例子1:

/**/
package pack;
class Ticket implements Runnable
{private static int ticket = 100;Object obj = new Object();boolean flag = true;public void run(){if(flag){while(true){synchronized(obj)//同步代码块{//0线程进来,持有obj锁,想要进show,需要this锁show();//同步代码块中包含同步函数  }}}elsewhile(true)show();}public synchronized void show()//同步函数{//1线程进来,持有this锁,想要进同步代码块,需要obj锁synchronized(obj)//同步函数中包含同步代码块{if(ticket > 0){try{Thread.sleep(5);}catch(Exception e){}System.out.println(Thread.currentThread().getName()+"....同步函数::"+ticket--);}  }}
}public class ThreadDemo
{public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();try{Thread.sleep(10);}catch(Exception e){}t.flag = false;t2.start();}
}
/*运行发现票没有卖完程序就不动了,也就是程序锁上了!(看视频11-15,6分10秒开始处的解释)
*/

例子2:死锁

/*
掌握词程序,面试可能用到!*/
package pack;//创建一个MyLock类,用于提供2个Object的对象锁,为了方便这两个对象被调用,设定其为static类型
class MyLock
{static Object locka = new Object();static Object lockb = new Object();
}//创建一个Test类实现Runnable接口
class Test implements Runnable
{private boolean flag;//设置一个布尔型的变量FlagTest(boolean flag){this.flag = flag;//提供构造函数为flag赋值}public void run(){if(flag){while(true){synchronized(MyLock.locka)//当flag=true,进入的同步代码块locka在外{System.out.println(Thread.currentThread().getName()+"...if locka ");synchronized(MyLock.lockb)//锁lockb在内{System.out.println(Thread.currentThread().getName()+"...if lockb ");}}}}else{while(true){synchronized(MyLock.lockb)//当flag=true,进入的同步代码块lockb在外{System.out.println(Thread.currentThread().getName()+"..else lockb ");synchronized(MyLock.locka)//锁locka在内{System.out.println(Thread.currentThread().getName()+"..else lockba ");}}}}}
}public class ThreadDemo
{public static void main(String[] args) {Thread t1 = new Thread(new Test(true));//线程0进去的时候进入if部分Thread t2 = new Thread(new Test(false));//线程1进去的时候进入else部分t1.start();t2.start();}
}
/*
结果:
Thread-0...if locka
Thread-1..else lockb
线程0拿着locka,想要进入下一层的同步代码块,但是没有lockb,因为此时线程1拿着lockb,没有locka,两个线程形成了死锁!
*/

参考阅读文章:
多线程详解
多线程详解

4、就业班多线程补充
补充1
  并发:指两个或多个事件在同一个时间段内发生。
  并行:指两个或多个事件在同一时刻发生(同时发生)。

补充2
  进程与线程


补充3
  线程的调度:
1)分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
2)抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

补充4
  主线程的内存原理

  多线程的内存图解

补充5:Thread和Runnable的区别
  相应的代码如下

实现Runnable接口创建多线程程序的好处:1.避免了单继承的局限性一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类实现了Runnable接口,还可以继承其他的类,实现其他的接口2.增强了程序的扩展性,降低了程序的耦合性(解耦)实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)实现类中,重写了run方法:用来设置线程任务创建Thread类对象,调用start方法:用来开启新线程

补充6:匿名内部类实现多线程
  代码如下

package com.itheima.demo05.InnerClassThread;
/*匿名内部类方式实现线程的创建匿名:没有名字内部类:写在其他类内部的类匿名内部类作用:简化代码把子类继承父类,重写父类的方法,创建子类对象合一步完成把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成匿名内部类的最终产物:子类/实现类对象,而这个类没有名字格式:new 父类/接口(){重复父类/接口中的方法};*/
public class Demo01InnerClassThread {public static void main(String[] args) {//线程的父类是Thread// new MyThread().start();new Thread(){//重写run方法,设置线程任务@Overridepublic void run() {for (int i = 0; i <20 ; i++) {System.out.println(Thread.currentThread().getName()+"-->"+"黑马");}}}.start();//线程的接口Runnable//Runnable r = new RunnableImpl();//多态Runnable r = new Runnable(){//重写run方法,设置线程任务@Overridepublic void run() {for (int i = 0; i <20 ; i++) {System.out.println(Thread.currentThread().getName()+"-->"+"程序员");}}};new Thread(r).start();//简化接口的方式new Thread(new Runnable(){//重写run方法,设置线程任务@Overridepublic void run() {for (int i = 0; i <20 ; i++) {System.out.println(Thread.currentThread().getName()+"-->"+"传智播客");}}}).start();}
}

补充7:线程的几类状态
  如下图

黑马毕向东Java课程笔记(day11):多线程(第一部分)——进程与线程+线程创建+线程安全与同步代码块+同步锁/死锁相关推荐

  1. 黑马毕向东Java课程笔记(day16-1-16-9):集合类(集合框架)——Map集合

    1.Map集合   Map集合的基本特点如下: 接口 Map<K,V>:将键映射到值的对象.一个映射不能包含重复的键:每个键最多只能映射到一个值.(但是值可以重复) K - 此映射所维护的 ...

  2. 黑马毕向东Java课程笔记(day07):面向对象(第三部分)继承+抽象类+模板方法设计模式+接口+final+继承补充(就业班)

      在这一部分中,我们将讲解有关继承的相关内容,包括继承的概述.继承的特点.super关键字.函数覆盖.子类的实例化过程.final关键字这几个部分的内容. 1.继承的概述以及特点 1.1.概述    ...

  3. 黑马毕向东Java课程笔记(day20-1——20-17)IO流:File类及相关方法、递归、递归的相关练习、Properties、PrintWriter类与PrintStream类、合并流与切割流

    1.File类概述   File是文件和目录路径名的抽象表示形式. 用来将文件或者文件夹封装成对象,方便对文件与文件夹的属性信息进行操作.   前面说到的"流",它只能操作数据,想 ...

  4. 黑马毕向东Java课程笔记(day19-11——19-22)IO字节流:字节流及其读取、字节流缓冲区、自定义字节流(读取)的缓冲区、读取键盘的输入、读取/写入转换流、流操作规律

    1.字节流--File   字节流的介绍 字符流:(一个字符2个字节16位) FileReader FileWriter. BufferedReader BufferedWriter字节流:(一个字节 ...

  5. 黑马毕向东Java课程笔记(day14-1——14-11):集合类(集合框架)——集合类分类与特点+List集合接口及其子类

    1.集合类特点   为什么出现集合类? 面向对象语言对事物的体现都是以对象的形式,所以为了方便对多个对象的操作,就对对象进行存储,集合就是存储对象最常用的一种方式.   数组和集合类同是容器,有何不同 ...

  6. 毕向东java基础笔记

    函数功能: pubic void getSum(int x, int y) { int ret = x+y; System.out.println(ret); } 这个功能定义的思想有问题,因为只为完 ...

  7. java synchronized块_Java多线程同步代码块Synchronized

    Java多线程同步代码块Synchronized Java中的每个对象都有一个与之关联的内部锁(Intrinsic lock). 这种锁也称为监视器(Monitor), 这种内部锁是一种排他锁,可以保 ...

  8. java 代码块同步,Java 同步代码块

    Java 同步代码块 1 什么是Java同步代码块 同步代码块可用于对方法的任何特定资源执行同步. 假设您的方法中有50行代码,但是您只想同步5行,则可以使用synchronized代码块. 如果将方 ...

  9. Java中多线程、多线程的实现方式、同步代码块的方式

    多线程 进程 线程 概念 目前的程序是单线程 线程的组成部分 代码实现多线程的方式 第一种方式 第二种方式 第三种方式 -- 线程池 第四种方式:Callable 线程状态 线程同步 临界资源 原子操 ...

最新文章

  1. 生成对抗网络(Generative Adversarial Network,GAN)
  2. 贝叶斯定理:AI 不只是个理科生 | 赠书
  3. yii2 layout main.php,yii2 – 如何将参数传递给mainLayoutAsset.php文件?
  4. php 获取发票内容,php – 如何从发票ID获取PayPal交易ID
  5. 增加mysql的sortbuffer_Mysql设置sort_buffer_size
  6. jersey rest webservice
  7. ubuntu终端下快捷键,字体放大缩小等【逐渐完善篇】
  8. c++ 虚函数,纯虚函数的本质区别
  9. 飞秋(FeiQ)已在计算机技术的世界里沉浸了十年
  10. 搭建 Hexo Blog
  11. 局域网ftp工具,ftp上传下载工具使用指南,5款好用的局域网ftp工具推荐
  12. Ubuntu使用ZTE MF832S上网卡拨号上网
  13. 用HTML+CSS做一个漂亮简单的个人网页——樱木花道篮球3个页面 学生个人网页设计作品 学生个人网页模板 简单个人主页
  14. centos添加互信
  15. js+css实现瀑布流
  16. 深入理解图优化与g2o:g2o篇
  17. 转:Flutter做出剑气效果
  18. 国内物联网产业仍处初级阶段 运营商NB-IoT大有可为
  19. Photoshop中的填充功能
  20. 清华大学老师的一席话

热门文章

  1. 服务器 python cant open file_python: can't open file 'h.py': [Errno 2] No such file or directory
  2. Hooks编程扫盲(一)-- useSWR
  3. 小升初报名修改密码出现内部服务器错误,小升初报名表填写错了怎么办
  4. 银行理财、保险、证券销售双录系统解决方案
  5. react中使用动画 react-transition-group
  6. 2022-2028年中国光存储行业发展战略规划及投资方向研究报告
  7. linux使用ps下查看进程运行的时间 【转】
  8. JavaScript DOM 编程艺术 (第二版)学习之3-4章
  9. 通过脚本控制播放音频
  10. 使用防火玻璃块创建安全,时尚的窗户和墙壁