黑马程序员---多线程

多线程

----------- android培训java培训、java学习型技术博客、期待与您交流! ------------

1.相关概念

1.1 并发

    概念:多个程序同时执行。

早期的计算机系统只执行一个程序,对计算机资源来说是一种浪费,所以在后期的出现的操作系统,使得系统可以一次进行多个程序,计算机资源得到很大系统的有效使用。

实现并发的原因

1)可以提高资源利用率

2)公平性– 程序对于计算机资源拥有同等的使用权

1.2 进程:

  概念:计算机正在运行的程序。

window上的进程

1.2.1 关于进程

1)我们的操作系统是多任务的,能够同时执行多个应用程序。

2)在某个瞬间,cpu只是运行一个进程。但是在1秒内,cpu就能运行多个进程。所以人们就会以为这些程序是同时执行的。

举例说明:

当我们将大文件从F盘复制到U盘,同时将E盘的东西复制到D盘,会发现就是这两个是同时在执行,但其实在花费的时间方面,并没有单个复制时执行时快,那是两个复制文件的进程中切换为很费时间。

3)多CPU系统具备真正硬件并行,能实现真正意义上的并行。

1.4 线程

1.4.1 定义

线程是进程的执行路径,一个进程至少有一个线程。

1.4.2 线程与进程的关系

1)线程比进程轻量级,比进程创建的快。

2)进程用于把资源集中在一起,线程则是在cpu上被调度执行的实体。

3)线程必须在某个进程中执行。

4)多个线程共享物理内存、磁盘、其他资源,同一个进程的线程共享一个进程的内存空间和其他资源。

5)一个进程中的多个线程,该多个线程全部在所属的进程的内存空间中运行。

6)多线程进程在单cpu系统中运行时,线程轮流执行,cpu在线程之间轮流快速切换,制造了线程并行的假象。

7)线程都有自己的状态:运行、阻塞、终止。。。

8)线程共享进程中的所有资源,同一个进程中的多个线程之间可以并发执行,更好改善了系统资源利用率。

1.4.3 多线程优缺点

1)进程之间不可以共享内存,但线程之间易于共享内存;

2)使用多线程来实现多任务并发比多进程的效率高;

3)Java语言内置了多线程功能支持;

4)线程太多会导致效率的降低,因为线程的执行依靠的是CPU的来回切换。

5)多线程是指一个进程中有多个线程。

2. 创建多线程的方式

2.1 继承Thread类

2.1.1 Thread类

Thread类是java中用来表示线程的类,需要创建线程就需要创建Thread类

线程是程序中的执行线程,Java虚拟机允许应用程序并发的运行多个执行线程;Thread类的语法格式如下所示:

流程:

1)定义继承Thread类;

2)重写Thread类的run方法;

3)创建该类的实例,调用start方法,该方法有两个作用,启动线程并且调用run方法。

package com.ping.Thread;
public class Demo01 {
    public static void main(String[] args) {
//创建MyThread的实例
       MyThread thread = new MyThread();
//调用start方法       
       thread.start();                        
       for (int i = 1; i <= 10; i++) {       
           System.err.println("main : " + i);
       }
    }
}
//继承Thread类
class MyThread extends Thread {
//重写了Thread类的run()方法                 
    public void run() {                         
       for (int i = 1; i <= 10; i++) {           
           System.out.println("myThread : " + i);
        }
    }
}
效果;
myThread : 1
myThread : 2
myThread : 3
main : 1
main : 2
main : 3
main : 4
main : 5
main : 6
main : 7
main : 8
main : 9
main : 10
myThread : 4
myThread : 5
myThread : 6
myThread : 7
myThread : 8
myThread : 9
myThread : 10

    发现main()方法和MyThread类中的循环交替执行。最终都执行完毕,运行结每次不同,因为线程都在获取cpu的执行权,cpu执行到谁谁就运行,单cpu下某个时刻只有一个线程在运行(多核不同),cpu做快速切换,看下去是在同时执行。

实际上程序经历了

1.启动主线程

public static void main(String[] args) {  
//java虚拟机调用main(),启动了主线程
    }

2.新的线程启动成为了活跃的线程

MyThread thread = new MyThread();       
//新线程启动,main线程暂时停止执行
       thread.start();
       for(int i = 1 ; i <=10 ; i ++ ){
           System.out.println("main : " + i);}

3.Java虚拟机会在线程和原来的主线程之间切换直到两者都执行完毕为止

2.1.2 线程执行原理

由于Java只是操作系统上的执行的一个进程,程序是cpu执行的,cpu实现多线程的步骤为

1.MyThread类的线程和main方法 抢cpu的执行权,抢到之后,执行一段时间,如果在特定时间没有执行完,交给另外一个程序执行。

2.物理上只是一个cpu的情况下,JVM采用抢占cpu资源,给不同的线程划分不同的时间段来执行程序,同一时间段只有一个线程在执行。

2.1.3 Java本身的多线程

Java本身也隐含了二个线程

1.main方法也有一个线程叫主线程

2.垃圾回收机制也是一个线程,是个后台线程

3.只要JVM启动,JVM就会自动启动垃圾回收机制。

2.1.4 线程细节

1.一个Thread实例就是一个对象,Thread也有变量和方法,也是在堆上生存和灭亡。

2.启动程序上的main方法执行运行在一个线程内,该线程叫主线程。

3.线程启动使用父类的start方法。

4.如果线程对象直接调用run方法,JVM不会当做线程来运行,只是当是普通的方法调用。

5.线程启动只能有一次,否则抛出异常。

一旦线程的run方法执行完毕后,该线程就不能重新再启动,该线程已经死亡。

6.不可以直接用Thread,而调用继承他的子类实例。

因为Thread可以调用run,但不能重写。没有重写run,什么也不执行。

匿名内部类的线程实现方式

public class Demo2 {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       new Thread(){                              //匿名内部类
           public void run(){
              for(int i = 1 ; i <= 10 ; i ++ ){
                  System.out.println("MyThread : " + i);
                 
              }
           }
          
       }.start();
       for(int i = 1 ;i <= 10 ; i ++){         //主方法的线程
           System.err.println("main : " + i);
          
       }
    }
}
效果:
main : 1MyThread : 1
MyThread : 2
MyThread : 3
MyThread : 4
MyThread : 5
MyThread : 6
MyThread : 7
MyThread : 8
MyThread : 9
MyThread : 10
 
main : 2
main : 3
main : 4
main : 5
main : 6
main : 7
main : 8
main : 9
main : 10

线程练习,模拟QQ

package com.ping.Thread;
public class Demo3 {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       Video v = new Video();
       Talk t = new Talk();
       for (int i = 1; i <= 10; i++) {
 //主线程                       
           System.err.println("main : " + i);
       }
     //调用start()方法
       v.start(); 
//调用start()方法                                          
       t.start();                                            
    }
}
//创建Thread的子类Video
class Video extends Thread {                                 
//复写run方法
    public void run() {
       while (true) {                                       
           System.out.println("视频中。。。。。。。。。。");
       }
    }
}
//创建Thread的子类Talk
Class Talk extends Thread {
//复写run方法                                       
    public void run() {
       while (true) {                                             
           System.out.println("输入中。。。。。。。。。。。。。。");
       }
    }
}

由上例可知新线程被启动后(调用start后),线程之间的执行不是按照main方法中的顺序执行,是抢占式的,谁抢到cpu的执行权,谁先执行。

2.2 Runnable接口

创建线程的第二种方法,使用Runnable接口。

2.2.1 使用Runnable的步骤

1.定义实现Runnable接口;

2.重写Runnable接口的run()方法,就是将线程运行的代码放入在run方法中;

3.通过Thread类建立线程对象;

4.将Runnable接口的子类对象作为实际参数,传递给Thread类构造方法;

5.调用Thread类的start方法开启线程,并调Runnable接口子类的run方法;

:(为什么要将Runnable接口的子类对象传递给Thread的构造方法呢?因为自定义的run方法所属对象是Runnable接口的子类对象,所以要让线程去执行指定对象的run方法)

public class RunnableDemo {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       //新建MyRun对象
       MyRun my = new MyRun();
       //新建Thread类的对象,将MyRun对象作为参数传递给它
       Thread t1 = new Thread(my);
       t1.start();
       for (int i = 1; i <= 10; i++) {
           System.err.println("main : " + i);
       }
    }
}
//新建MyRun实现了Runnable接口
class MyRun  implements Runnable{
    @Override
    public void run() {
       //类中只有一实现方法
       // TODO Auto-generated method stub
       for (int i = 1; i <= 10; i++) {
           System.out.println("MyRun : " + i);
       }
    }  
}
输出:
MyRun : 1main : 1
main : 2
main : 3
main : 4
main : 5
main : 6
main : 7
main : 8
main : 9
main : 10
MyRun : 2
MyRun : 3
MyRun : 4
MyRun : 5
MyRun : 6
MyRun : 7
MyRun : 8
MyRun : 9
MyRun : 10

程序解释:Thread类就像是一个工人,而Runnable的实现类的对象就是这个工人的工作,而Runnable的实现类的对象就是这个工人的工作(通过构造方法的传递)。Runnable接口中只有一个方法run方法,当我们把Runnable的子类对象传递给Thread的构造函数时,实际上就是让Thread取得run方法,就是给Thread一项任务。

2.3 线程的常见方法

2.3.1 实例
package com.ping.Thread;
public class Demo4 {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       ThreadDemo t1 = new ThreadDemo();
//没有设置就默认值
       System.out.println(t1.getName());    
//设置了就有自定义名了           
       ThreadDemo t2 = new ThreadDemo("one");
       System.out.println(t2.getName()); 
//设置方法名称                    
       t2.setName("threadDemo1");
//没有指定线程的优先级,默认为5                             
       System.out.println(t1.getPriority());
//设置线程1为最高级别              
       t1.setPriority(Thread.MAX_PRIORITY);
//再显示线程级别                 
       System.out.println(t1.getPriority());
//把线程2设为最低级别                  
       t2.setPriority(Thread.MIN_PRIORITY);
//返回其级别数               
       System.out.println(t2.getPriority()); 
//线程1的ID                
       System.out.println(t1.getId());                       
       t1.start();
       t2.start();
    }
}
class ThreadDemo extends Thread {
    public ThreadDemo() {
    }
    public ThreadDemo(String name) {
       super(name);
    }
    public void run() {
       for (int i = 1; i <= 10; i++) {
           System.out.println(super.getName() + "i : " + i + " name : "
                  + this.getName() + " id : " + this.getId() + " 优先级:
                  + this.getPriority());          System.err.println(Thread.currentThread().getName().equals(
                  this.getName()));
       }
    }
}
效果:
Thread-0
one
5
10
1
8
Thread-0i : 1 name : Thread-0 id : 8 优先级 :10
threadDemo1i : 1 name : threadDemo1 id : 9 优先级 :1
Thread-0i : 2 name : Thread-0 id : 8 优先级 :10
Thread-0i : 3 name : Thread-0 id : 8 优先级 :10
Thread-0i : 4 name : Thread-0 id : 8 优先级 :10
Thread-0i : 5 name : Thread-0 id : 8 优先级 :10
Thread-0i : 6 name : Thread-0 id : 8 优先级 :10
Thread-0i : 7 name : Thread-0 id : 8 优先级 :10
true
true
true
true
true
true
true
true
trueThread-0i : 8 name : Thread-0 id : 8 优先级:10
Thread-0i : 9 name : Thread-0 id : 8 优先级 :10
 
Thread-0i : 10 name : Thread-0 id : 8 优先级 :10
true
true
threadDemo1i : 2 name : threadDemo1 id : 9 优先级 :1
true
threadDemo1i : 3 name : threadDemo1 id : 9 优先级 :1
true
threadDemo1i : 4 name : threadDemo1 id : 9 优先级 :1
true
threadDemo1i : 5 name : threadDemo1 id : 9 优先级 :1
true
threadDemo1i : 6 name : threadDemo1 id : 9 优先级 :1
true
threadDemo1i : 7 name : threadDemo1 id : 9 优先级 :1
true
threadDemo1i : 8 name : threadDemo1 id : 9 优先级 :1
true
true
true
threadDemo1i : 9 name : threadDemo1 id : 9 优先级 :1
threadDemo1i : 10 name : threadDemo1 id : 9 优先级 :1

2.4 线程的状态


1.new创建线程对象

2.调用线程的start方法 ---> 进入可运行的状态

3.抢占cpu资源

1)抢到进入运行状态

2)抢不到进入阻塞状态 -->其他线程运行完后,进入可运行状态

4.     线程死亡 -->线程执行完毕

2.4.1 状态详解

    新状态:创建Thread实例,还没有调用start方法之前线程所处的状态。是一个Thread类的对象,但是还不是执行线程。

可运行状态:调用start()方法线程可进入可运行状态,具备运行资格,没有运行权。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。

运行状态:线程调度程序从可运行池中选择一个线程开始运行,线程正在执行。

等待/阻塞/睡眠状态:线程具备运行资格,但是线程不可运行。这是线程有资格运行,但没有运行权。

死状态:线程的run方法完成。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

阻止线程执行

等待/阻塞/睡眠状态

方法Thread.sleep(long millis)和Thread.sleep(long millis, intmanos) : 静态方法强制当前正在执行的线程休眠(暂停运行)。当线程睡眠时,在苏醒前不会返回到可运行状态。当睡眠时间到期后,则返回可运行状态。

原因:线程执行太快,或者需要强制进入下一轮,因为Java规范不保证合理轮换。

位置:为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠。

package com.ping.Thread;
public class ThreadSleepDemo {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       new SleepDemo().start();
       for(int i = 1 ; i <= 500 ; i ++ ){
          
           System.err.println(Thread.currentThread().getName()+ ":" + i);
       }
    }
}
 
class SleepDemo extends Thread {
    public SleepDemo() {
    }
    public SleepDemo(String name) {
       super(name);
    }
    public void run() {
       try {
           Thread.sleep(10);                                                    //一开始就进入睡眠状态,确保main能先运行
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           for(int i = 1 ; i  <= 500 ; i ++ ){
              System.out.println("myThread : " + i);
//当到 i = 100 时也进入睡眠状态,所以在i ==100时,该线程会暂停
              if(i==100 ){                                               
                  try {
                     Thread.sleep(10);
                  } catch (InterruptedException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                  }
                 
              }
             
           }
       }
    }
}

3. 由卖票程序看程序的安全问题

3.1 实例一代码:使用Thread实现方法

package com.ping.Thread;
public class TicketDemo {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       Ticket t1 = new Ticket("一号");
       Ticket t2 = new Ticket("二号");
       t1.start();
       t2.start();
    }
}
class Ticket extends Thread{
    private int tickets = 10 ;
    Ticket(){}
    Ticket(String name){
       super(name);
    }
    public void run(){
       while(true){
           if(tickets > 0){
              System.out.println(this.getName() + "窗口销售:" + tickets + "号票");
              tickets --;
           }else{
              System.out.println("票已卖完。。。。。");
              break;
           }
       }
    }
}
效果:
一号窗口销售:10号票
一号窗口销售:9号票
一号窗口销售:8号票
二号窗口销售:10号票
二号窗口销售:9号票
二号窗口销售:8号票
二号窗口销售:7号票
二号窗口销售:6号票
二号窗口销售:5号票
二号窗口销售:4号票
二号窗口销售:3号票
二号窗口销售:2号票
二号窗口销售:1号票
票已卖完。。。。。
一号窗口销售:7号票
一号窗口销售:6号票
一号窗口销售:5号票
一号窗口销售:4号票
一号窗口销售:3号票
一号窗口销售:2号票
一号窗口销售:1号票
票已卖完。。。。。

注意:每个票号都被打印了2次,2个线程都在卖自己的10张票,所以票数不是原来的10张,而是20张。因为我们创建了2个Thread对象,每个对象都维护自己的实例对象,互不干涉,所以我们需要两个线程去同时处理一个资源,这可以用到static

3.2 实例二使用Thread实现卖票程序二

package com.ping.Thread;
public class TicketDemo {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       Ticket t1 = new Ticket("one");
       Ticket t2 = new Ticket("two");
       t1.start();
       t2.start();
    }
}
class Ticket extends Thread{
    private static int tickets = 10 ;
    Ticket(){}
    Ticket(String name){
       super(name);
    }
    public void run(){
       while(true){
           if(tickets > 0){
              System.out.println(this.getName() + "窗口销售:" + tickets + "号票");
              tickets --;
           }else{
              System.out.println("票已卖完。。。。。");
              break;
           }     
       }
    }
}
效果:
one窗口销售:10号票
two窗口销售:10号票
two窗口销售:9号票
two窗口销售:8号票
two窗口销售:7号票
two窗口销售:6号票
two窗口销售:5号票
two窗口销售:4号票
two窗口销售:3号票
two窗口销售:1号票
票已卖完。。。。。
one窗口销售:2号票
票已卖完。。。。。

由结果可得:

出现了新的问题,10号票被卖了两次。

原因:当tickets === 10,线程1在执行完if(ticket>0)代码,执行完输出语句打印出票数10时,准备执行tickets – 时,操作系统将cpu切换到线程2上执行if(tickets > 0)操作,此时tickets 仍为10,所以线程2仍打印出票数10。

总结:当有多个线程在执行同一线程共享数据时,一个线程只执行一部分还没有执行完,另一个线程参与进来,导致共享数据错误。

结论:多条操作共享数据的语句,只能让一个线程都执行完,在执行的过程中,其他线程不可以参与执行。

3.3 实例三,使用Runnable实现卖票功能

package com.ping.Thread;
public class TicketDemo2 {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       Ticket2 t = new Ticket2();
       Thread th1 = new Thread(t,"one");
       Thread th2 = new Thread(t,"two");
       Thread th3 = new Thread(t,"three");
       Thread th4 = new Thread(t,"four");
       th1.start();
       th2.start();
       th3.start();
       th4.start();
    }
}
class Ticket2 implements Runnable{
    int tickets =10;
    @Override
    public void run() {
       // TODO Auto-generated method stub
       while(true){
           if(tickets > 0){
              try {
                  Thread.sleep(100);                                         //第张票都停止一段时间
              } catch (InterruptedException e) {
                  // TODO Auto-generated catch block
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName()+ "销售窗口:" + tickets + "号票");
              tickets --;
             
           }else{
              System.out.println("票已卖完。。。。。。。。。");
              break;
           }     
       }  
    }  
}
结果:
one销售窗口:10号票
two销售窗口:9号票
four销售窗口:9号票
three销售窗口:7号票
two销售窗口:6号票
one销售窗口:5号票
four销售窗口:4号票
three销售窗口:3号票
two销售窗口:2号票
one销售窗口:2号票
票已卖完。。。。。。。。。
three销售窗口:2号票
票已卖完。。。。。。。。。
four销售窗口:-1号票
票已卖完。。。。。。。。。

由结果可知运行票数出现了0与负数,还有同一张票被反复销售。

解决这问题的方案就是同步代码块。

3.4同步代码块

对于多线程的安全问题的解决文字就是同步代码块

格式:

synchronized(对象){
                 //需要被同步的代码
           }

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

3.4.1 Thread线程安全问题解决方案一
package com.ping.Thread;
public class LockDemo {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       TicketClock tc1 = new TicketClock("one");
       TicketClock tc2 = new TicketClock("two");
       tc1.start();
       tc2.start();
      
    }
}
class TicketClock extends Thread{
    private static int tickets = 100;
    Object obj = new Object();
    TicketClock() {
    }
    TicketClock(String name) {
       super(name);
    }
    public void run(){
       while(true){
           synchronized(obj){
              if(tickets > 0){
                  System.out.println(this.getName() + "窗口销售" + tickets  + "号票");
                  tickets --;                
              }else{
                  System.out.println("票已经卖完了");
                  break;
              }            
           }         
       }     
    }
}
效果:
one窗口销售10号票
one窗口销售9号票
one窗口销售8号票
one窗口销售7号票
one窗口销售6号票
one窗口销售5号票
one窗口销售4号票
one窗口销售3号票
two窗口销售10号票
two窗口销售1号票
票已经卖完了
one窗口销售2号票
票已经卖完了

使用Object对象做为锁,却没有作为,因为每个锁持有的对象虽然不一样,但是都Object的子类,所以用Object做锁,解决不了多线程安全的问题。

3.4.2 Thread线程安全问题解决方案二---让多个线程使用同一个锁
package com.ping.Thread;
public class LockDemo2 {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       LockTicket lt1 = new LockTicket("one");
       LockTicket lt2 = new LockTicket("two");
       lt1.start();
       lt2.start();
    }
}
class LockTicket extends Thread {
    private static int tickets = 10;
//使用静态,让多个线程用同一把锁                                        
    static Object obj = new Object();                                    
    LockTicket() {
    }
    LockTicket(String name) {
       super(name);
    }
    public void run() {
       synchronized (obj) {
           while (true) {
              if (tickets > 0) {
                  System.out
                         .println(this.getName() + "窗口销售" + tickets + "号票");
                  tickets--;
              } else {
                  System.out.println("票已卖完。。。。。。");
                  break;
              }
           }
       }
    }
}
效果:
one窗口销售10号票
one窗口销售9号票
one窗口销售8号票
one窗口销售7号票
one窗口销售6号票
one窗口销售5号票
one窗口销售4号票
one窗口销售3号票
one窗口销售2号票
one窗口销售1号票
票已卖完。。。。。。
票已卖完。。。。。。

如果使用静态变量来存储票数那么导致该变量的声明周期很长,当票为0时,都没有释放该变量。

缺点:不太符合面向对象的编程思想,也创建了多余的锁对象。

总结

火车票售票如果创建多个Thread对象,调用各自的对象的start方法是在使用各自的数据,如果把数据设置为静态(数据被所有对象所共享),再使用同步机制解决,可以解决问题,但是static的声明周期太长,并且创建了多余的锁对象。

3.4.3 Runnable接口线程安全问题解决方案

  综上所述,使用Thread创建线程的方式很麻烦,所以还是试试第二个方式,实现Runnable接口。

package com.ping.Thread;
public class MyRunTicketDemo {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       MyRunTicket mt = new MyRunTicket();
       Thread t1 = new Thread(mt,"one");
       Thread t2 = new Thread(mt,"two");
       Thread t3 = new Thread(mt,"three");
       Thread t4 = new Thread(mt,"four");
       t1.start();
       t2.start();
       t3.start();
       t4.start();
    }
}
class MyRunTicket implements Runnable{
    private int tickets = 10 ;
    @Override
    public void run() {
       // TODO Auto-generated method stub
       while(true){
           synchronized(this){
              if(tickets > 0){
                  System.out.println(Thread.currentThread().getName() + "卖" + this.tickets + "号票!");
                  try {//线程睡眠一下,等其他线程来执行
                     Thread.sleep(10);                                                                   
                  } catch (InterruptedException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                  }
                  tickets -- ;
              }else{
                  System.out.println("卖已卖完!");
                  break;
              }
           }
       }
    }
}
效果:
one卖10号票!
one卖9号票!
one卖8号票!
one卖7号票!
four卖6号票!
four卖5号票!
three卖4号票!
three卖3号票!
three卖2号票!
three卖1号票!
卖已卖完!
卖已卖完!
卖已卖完!
卖已卖完!

3.5 关于线程同步

3.5.1 总结

1)同步的前提必须有两个或者两个以上线程,多个线程使用的是同一个锁。

2)就是当一个线程在执行卖票操作时,要确保其他线程不可以插入。

3.5.2 前提

1)必须是多个线程使用同一个锁。

2)必须保证同步中只能有一个线程在运行。

3.5.3 特点

1)解决了线程安全问题,(一个共享资源被多个线程同时访问,就可能出现线程安全问题)

2)即使获取了cpu的时间片,没有对象锁也无法执行。

3)单线程无需同步

3.5.4 缺点

1)多个线程都需要判断锁较为消耗资源(加了锁后,一个线程进入该方法后,就持有了锁,在释放锁之前,其他线程即使拥有了cpu时间,没有对象锁也无法执行,只能等该线程执行完该方法,释放锁)

2)当线程相当多时,因为每个线程都会去判断同步上的锁,很费资源,会降低程序运行时的效率。

3.5.5 实现接口与继承父类的区别

1)实现方法避免了单继承的局限性。

2)继承Thread线程代码放在子类 run方法中,而实现Runnable线程代码放在子类run方法中。

4. 同步

4.1 锁

4.1.1 概念

每个Java对象都有一个锁对象,而且只有一把锁匙。

4.1.2 锁对象的创建

1)可以使用this关键字作为锁对象。

2)使用所有类的字节码文件对应的Class文件,作为锁的对象----> 类名.class 或者 对象.getClass()。

4.1.3 总结

1)只能同步方法(代码块),不能同步变量或者类;

2)不必同步类中的所有方法,类可以同时具有同步方法和非同步方法;

3)一个线程获得了对象的锁,其他线程不可以进入该对象的同步方法;

4)同步会影响性能(可能会导致死锁),优先考虑同步代码块。

4.2 同步函数

4.2.1 相关概念

1)函数需要被对象调用,那么函数都有一个对象所属的对象引用,是this,所以同步函数使用的锁是this。

public synchronized void method(){
          //该方法就是同步方法,同步是需要锁的,所有函数都有一个所属对象引用,就是this,所以同步函数使用的锁就是this
    }

2)同步函数被静态修饰后使用什么锁,由于静态方法中不可以定义this静态进内存时,内存可能还没有本类对象,但是一定有该类对应的字节码文件对象,通过类名.class获取的就是该方法所有类的字节码。

public synchronized static void method(){
    }

3)同步方法可以转换为同步代码块,该例子中的同步方法等价于同步代码块

public synchronized static void method(){
    }
    public void method1(){
       synchronized(this){}
    }

4.3单例懒汉式同步

4.3.1 方法一
public class Single {
    private static Single sin = null;
    private Single() {
    }
    public static synchronized Single getInstance() {
       if (sin == null) {
           sin = new Single();
       }
       return sin;
    }
}
4.3.2 方法二
class Single2 {
    private static Single2 sin = null;
    private Single2() {
    }
    public static Single2 getInstance() {
       synchronized (Single2.class) {
           if (sin == null) {
              sin = new Single2();
           }
           return sin;
       }
    }
}

方法一与方法二其实是一样的,都是全部同步。

其实只需要当sin 引用为null的时候进行线程同步即可。

4.3.3 方法三
class Single3 {   //懒汉式加入同步会比较低效,所以在其同步前加入判断,若sin == null 才同步,这样会较为高效
    private static Single3 sin = null;
    private Single3() {
    }
    public static Single3 getInstance() {
       if (sin == null) {
           synchronized (Single3.class) {
              if (sin == null) {
                  sin = new Single3();
              }
           }
       }
       return sin;
    }
}

4.4 死锁

例子:“哲学家就餐问题”,5个哲学家吃中餐。但只有5只筷子,放在他们的位子中间,若每个哲学家都拿着自己的筷子,那么所有人都会饿死,这会产生死锁。

4.4.1概念

  每个人都拥有其他人需要的资源,同时又需要其他的资源,但每个人在得到自己想要的资源前都不会放弃自己拥有的资源的时候就会产生死锁。

4.4.2出现死锁的情况

1.两个任务以相反的顺序申请两个锁。

2.线程T1获得锁L1,线程T2获得锁L2 ,然后T1申请获得锁L2,同时T2申请获得锁L1,此时两个线程将要永久阻塞,死锁出现。

如果一个类可能发生死锁,那么并不意味每次都会出现死锁,只是表示可能。要避免程序中出现死锁。

如某个程序需要访问两个文件,当进程中的两个线程分别各锁住了一个文件,那它们都在等待着对方解锁另一个文件,而这永远不会发生。

4.4.3 避免死锁

下例就是一个死锁的例子,中国人拿着刀叉,问拿筷子的美国人要筷子,而美国人说,先给刀叉再给筷子,而中国也是不给筷子就不给刀叉,两方于是就进入了胶着状态,这就是死锁。

package com.ping.JFrame;
public class DeadLock {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
      //中国人
       new Thread(new Runnable(){                           
           @Override
           public void run() {
              // TODO Auto-generated method stub
//中国人拿着刀叉
              synchronized("刀叉"){                         
                  System.out.println(Thread.currentThread().getName() + ":你不给我筷子,我就不给你刀叉");
                  try {
                     Thread.sleep(10);
                  } catch (InterruptedException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                  }
                  synchronized("筷子"){
           System.out.println(Thread.currentThread().getName() + ": 给你刀叉");
                  }
              }
           }
       },"中国人").start();
       //美国人
       new Thread(new Runnable(){                                   
           @Override
           public void run() {
              // TODO Auto-generated method stub
//美国人拿着筷子
              synchronized("筷子"){                                 
                  System.out.println(Thread.currentThread().getName() + ":你给我刀叉,我再给你筷子");
                  try {
                     Thread.sleep(10);
                  } catch (InterruptedException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                  }
                  synchronized("刀叉"){
                      System.out.println(Thread.currentThread().getName() + ": 好吧,把筷子给你。");
                    
                  }
              }
           }
       },"美国人").start();
    }
}
效果:
美国人:你给我刀叉,我再给你筷子
中国人:你不给我筷子,我就不给你刀叉
程序一直卡着,没法运行。

5. 线程间的通信

5.1 概念

线程间的通信其实就是多个线程在操作同一个资源,但操作动作不同。

5.2 例子:生产者与消费者

如果有多个生产者与消费者,一定要使用while循环判断标记,然后再使用notifyAll唤醒。否则只用notify容易出现只唤醒本方线程情况,导致程序中所有线程都在等待。

例如一个数据空间,划分为两个部分,一部分存储人的姓名,一部分存储性别,我们开启一个线程,不停地向其中存储姓名和性别(生产者),开启另一个线程从数据存储空间中取出数据(消费者)。

由于是多线程,就需要考虑,假如生产者刚向数据存储空间中加了一个人名,还没有来得及添加性别,cpu就切换到了消费者的线程,消费者就会将这个人的姓名和上一个人的性别进行了输出。

还有一种情况就是生产者生产了若干次数据,消费者才开始取数据,或者消费者取出数据后,没有等到消费者放入新的数据,消费者又重复地取出自己已经取过的数据。如下例:

下述代码中,Producer和Consumer类的内部都维护着一个Person类型的P成员变量,通过构造函数进行赋值,在main方法中创建了一个Person对象,将其同时传递给Producer和Consumer对象,所以Producer和Consumer访问的是同一个Person对象。并启动了两个线程。

package com.ping.JFrame;
public class CustomerDemo {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       Person p = new Person();
       Producer pro = new Producer(p);
       Consumer con = new Consumer(p);
       Thread t1 = new Thread(pro,"生产者");
       Thread t2 = new Thread(con,"消费者");
       t1.start();
       t2.start();
    }
}
//使用Person作为存储空间
class Person{
    String name;
    String gender;
}
//生产者
class Producer implements Runnable{
    Person p;
    public Producer(){}
    public Producer(Person p ){
       this.p = p;
    }
    @Override
    public void run() {
       // TODO Auto-generated method stub
       int i = 0 ;
       while(true){
           if(i % 2 == 0){
              p.name = "jack";
              p.gender = "man";
           }else{
              p.name = "莉莉";
              p.gender = "女人";
           }
           i ++;
       }  
    }
}
//消费者
class Consumer implements Runnable{
    Person p ;
    public Consumer(Person p) {
       super();
       this.p = p;
    }
    public Consumer() {
       super();
       // TODO Auto-generated constructor stub
    }
    @Override
    public void run() {
       // TODO Auto-generated method stub
       while(true){
           System.out.println("name : " + p.name + "--gender : " + p.gender);
       }
}
效果:
name : 莉莉--gender : man
name : 莉莉--gender : man
name : jack--gender : man
name : jack--gender : 女人
name : 莉莉--gender : 女人
name : jack--gender : man
name : 莉莉--gender : 女人
name : 莉莉--gender : man
name : jack--gender : man
name : 莉莉--gender : 女人
name : jack--gender : man
name : jack--gender : man
name : jack--gender : 女人
name : jack--gender : man
name : jack--gender : man
name : 莉莉--gender : 女人
name : 莉莉--gender : man
name : jack--gender : man
name : jack--gender : man
name : jack--gender : man

由结果看出现了很多 jack—女人 , 莉莉--- man的输出,很显示是出现了线程安全问题。

5.3 用同步解决问题

package com.ping.JFrame;
public class CustomerDemo {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       Person p = new Person();
       Producer pro = new Producer(p);
       Consumer con = new Consumer(p);
       Thread t1 = new Thread(pro, "生产者");
       Thread t2 = new Thread(con, "消费者");
       t1.start();
       t2.start();
    }
}
// 使用Person作为存储空间
class Person {
    String name;
    String gender;
}
// 生产者
class Producer implements Runnable {
    Person p;
    public Producer() {
    }
    public Producer(Person p) {
       this.p = p;
    }
    @Override
    public void run() {
       // TODO Auto-generated method stub
       int i = 0;
       while (true) {
  //加上同步
           synchronized (p) {              
              if (i % 2 == 0) {
                  p.name = "jack";
                  p.gender = "man";
              } else {
                  p.name = "莉莉";
                  p.gender = "女人";
              }
              i++;
           }
       }
    }
}
// 消费者
class Consumer implements Runnable {
    Person p;
    public Consumer(Person p) {
       super();
       this.p = p;
    }
    public Consumer() {
       super();
       // TODO Auto-generated constructor stub
    }
    @Override
    public void run() {
       // TODO Auto-generated method stub
       while (true) {
           synchronized (p) {                     //加上同步
              System.out.println("name : " + p.name + "--gender : "
                     + p.gender);
           }
       }
    }
}
效果
name : jack--gender : man
name : jack--gender : man
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : jack--gender : man
name : jack--gender : man
name : jack--gender : man
name : jack--gender : man
name : jack--gender : man
name : jack--gender : man
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人
name : 莉莉--gender : 女人

由结果上看:屏幕上没有出现jack –女人 , 莉莉—man的结果,说明了我们解决了同步的问题,但发现,效果不如理想,因为结果应该是先输出java—man 然后再输出莉莉—女人,这样循环下去,很明显出现这样的结果是因为生产者生产了若干个数据后,消费者才开始取数据,或者是消费者取完数据后,生产者还没有产生新的数据,所以消费者又重复输出之前取出过的数据。

6. 等待唤醒机制

6.1 相关命令

wait:告诉当前线程放弃执行权,并放弃监视器(锁)并进入睡眠状态,直到其他线程持有获得执行权,并持有了相同的监视器(锁)并调用notify为止。

notify:唤醒持有同一个监视器(锁)中调用wait的第一线程,例如:餐馆有空位置,等候冒险岛的顾客优先入座。注意:被唤醒的线程进入可运行状态,等待cpu的执行权。

notifyall:唤醒持有同一监视器中调用wait的所有线程。

消费者问题:通过设置一个标记,表示数据的(存储空间的状态)如:当消费者读取了(消费了一次)一次数据之后可以将标记以为false,当生产者生产了一个数据,将标记改为true,也就是只有标记为true的时候,消费者才能取走数据,标记为false时生产者才能生产数据。

如下代码:

package justTest;
public class Demo10 {
    public static void main(String[] args) {
       Person p = new Person();
       Producer pro = new Producer(p);
       Consumer con = new Consumer(p);
       Thread t1 = new Thread(pro, "生产者");
       Thread t2 = new Thread(con, "消费者");
       t1.start();
       t2.start();
    }
}
// 使用Person作为数据存储空间
class Person {
    String name;
    String gender;
    boolean flag = false;
    public synchronized void set(String name, String gender) {
       if (flag) {
           try {
              wait();
           } catch (InterruptedException e) {
              // TODO Auto-generated catch block
              e.printStackTrace();
           }
       }
       this.name = name;
       this.gender = gender;
       flag = true;
       notify();
    }
    public synchronized void read() {
       if (!flag) {
           try {
              wait();
           } catch (InterruptedException e) {
              // TODO Auto-generated catch block
              e.printStackTrace();
           }
       }
       System.out.println("name:" + this.name + "----gender:" + this.gender);
       flag = false;
       notify();
    }
}
// 生产者
class Producer implements Runnable {
    Person p;
    public Producer() {
    }
    public Producer(Person p) {
       this.p = p;
    }
    @Override
    public void run() {
       int i = 0;
       while (true) {
           if (i % 2 == 0) {
              p.set("jack", "man");
           } else {
              p.set("莉莉", "女人");
           }
           i++;
       }
    }
}
// 消费者
class Consumer implements Runnable {
    Person p;
    public Consumer() {
    }
    public Consumer(Person p) {
       this.p = p;
    }
    @Override
    public void run() {
       while (true) {
           p.read();
       }
    }
}
效果:
name:莉莉----gender:女人
name:jack----gender:man
name:莉莉----gender:女人
name:jack----gender:man
name:莉莉----gender:女人
name:jack----gender:man
name:莉莉----gender:女人

解决了线程通信的安全问题。使程序达到了理想的效果。

线程间通信其实就是多个线程在操作同一个资源,但操作动作不同,wait,notify,notifyAll都使用在同步上,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。

wait()sleep()有什么区别!

1) wait()释放资源,释放锁。是Object的方法。

2) sleep() :如果线程进入sleep()释放资源,不释放锁。是Thread的方法。

notify 与notifyAll的区别

1) notify:只唤醒本方线程,导致所有线程都在等待。

2) notifyAll:唤醒所有线程。

7. 线程的生命周期

线程的生命周期分为:

1)正常终止当线程的run()执行完毕,线程死亡

2)使用标记停止线程

Stop()方法已经过时,不能使用这个方法停止线程,只要将运行代码的循环体停止,线程就结束。如:用计数器

package justTest;
public class StopThreadDemo {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       StopThread st = new StopThread();
       Thread th = new Thread(st, "线程1");
       th.start();
       for (int i = 1; i <= 100; i++) {
           if (i == 50) {
//当 i = 50时st.tag为false,线程终止
              System.out.println("main i :" + i);        
              st.tag = false;
           }
       }
    }
}
class StopThread implements Runnable {
//循环运行标志
    public boolean tag = true;                        
    public void run() {
       int i = 0;
       while (tag) {
           i++;
           System.out.println(Thread.currentThread().getName() + "i :" + i);
       }
    }
}
效果:
线程1i :1
线程1i :2
main i :50
线程1i :3

注意:当执行main()方法时,计数器i==50时,将标记tag变为false,但cpu不一定马上回到线程1中,所以线程1并不会马上终止。

8. 后台线程

概念:就是隐藏起来一直在默默运行的线程,直到进程结束。

实现

setDaemon(boolean bo)

特点

1)当所有非后台线程结束时,程序也就终止了同时还会杀死进程中的所有后台线程,也就是说只要有非后台线程在运行,程序就不会终止,执行main方法的主线程就是一个非后台线程。

2)必须在启动线程之前(调用start()方法之前)调用setDaemon(true)方法,才可以把该线程设置为后台线程。

3)一旦main()执行完毕,那么程序应付终止,JVM也就退出了。

4)可以使用isDaemon()测试该线程是否为后台线程(守护线程)。

package justTest;
public class Demo9 {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       QQUpdate qu = new QQUpdate();
       Thread th = new Thread(qu,"qq更新");
       th.setDaemon(true);
       th.start();
       System.out.println(th.isDaemon());
       System.out.println("HelloWorld");
    }
}
class QQUpdate implements Runnable{
    int i = 0 ;
    @Override
    public void run() {
       // TODO Auto-generated method stub
       while(true){
           System.out.println(Thread.currentThread().getName() + "检测是否有可用更新");
           i ++;
           try {
              Thread.sleep(10);
           } catch (InterruptedException e) {
              // TODO Auto-generated catch block
              e.printStackTrace();
           }
           if(i == 100){
              System.out.println("有可用更新,是否升级?");
              break;
           }
       }
      
    }  
}
效果:
 true
HelloWorld
qq更新检测是否有可用更新
qq更新检测是否有可用更新
qq更新检测是否有可用更新
qq更新检测是否有可用更新

当非后台线程结束后,后台线程也随之消亡。

9. Thread的join方法

1)当A线程执行到了B线程Join方法时A就会等待,等B线程执行完A才会执行,Join可以用来临时加入线程执行。

2)通过Join方法加入到线程中,只有当JoinThread线程执行完,主线程才会执行。

package justTest;
public class ThreadJoinDemo {
    /**
     * @param args
     */
    public static void main(String[] args) {
       // TODO Auto-generated method stub
       JoinThread jt = new JoinThread();
//新的线程
       Thread th = new Thread(jt,"one");                  
       th.start();
       int i = 0 ;
       while(i < 10){
           if(i ==5){
              try {
//将th加入到主线程中去,当th执行完后,主线程才执行
                  th.join();                           
              } catch (InterruptedException e) {
                  // TODO Auto-generated catch block
                  e.printStackTrace();
              }
           }
           System.out.println(Thread.currentThread().getName() + "i : " + i);
           i ++;
       }
    }
}
class JoinThread implements Runnable{
    @Override
    public void run() {
       // TODO Auto-generated method stub
       int i = 0 ;
       while(i < 10){
           try {
//睡眠0.1秒
              Thread.sleep(100);                                         
           } catch (InterruptedException e) {
              // TODO Auto-generated catch block
              e.printStackTrace();
           }
           System.out.println(Thread.currentThread().getName() + "i :" + i);
           i ++;
       }
    }
}
结果:
maini : 0
maini : 1
maini : 2
maini : 3
maini : 4
onei :0
onei :1
onei :2
onei :3
onei :4
onei :5
onei :6
onei :7
onei :8
onei :9
maini : 5
maini : 6
maini : 7
maini : 8
maini : 9

使用Thread类的join方法,将th对应的线程合并到使用 th.joing语句的线程中,当合并后,如果join进去的线程没有执行完的话,被“join”的线程是不会执行的。

带整数的join方法是指定合并时间,有纳秒与毫秒级别。

----------- android培训java培训、java学习型技术博客、期待与您交流! ------------







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值