java多线程知识浅析

多线程

学习线程先看三个概念:

  • 程序:程序(program)是一个指令的集合。
  • 进程:进程(process)正在进行中的程序,是一个静态的概念。

    • 进程是程序的一次静态的执行过程,占用特定的地址空间。
    • 每个进程都是独立的,有三部分组成,cup、data、code。
    • 缺点:内存的浪费,cpu的负担。
  • 线程:是进程中的一个”单一的连续控制流程”。

    • 线程又被称为轻量级进程。
    • 一个进程可以有多个并行的线程。
    • 一个进程中的线程共享相同的内存单元——>可以访问相同的变量和对象,而且他们从 同一堆中分配对象——>通讯,数据交换,同步操作

> 线程的生命周期及五种基本形态:

先看一张关于线程生命周期的图(图是盗的,但想表达意思是真的【捂脸】)

线程的五种基本形态:

  • 1、新生状态(对应上图的new):当线程被创建即进入新生状态,相当于Thread thread = new Thread();
  • 2、就绪状态(对应Runable):当线程执行了.start()方法,即进入就绪状态,随时等待cpu的调度,这里要注意,进入就绪状态并不能理解为线程开始执行。 (thread.start();)
  • 3、运行状态(对应Running):当cpu对已经进入就绪状态的线程进行调度的时候,线程才算开始运行。也可以说是线程要想运行,必须是出于就绪状态,并且等待cup对其进行调度。
  • 4、阻塞状态(对应Blocked):正在运行的线程,由于某种特定的原因,cpu暂时放弃对它的调度,停止执行,即进入阻塞状态,直到此线程进入就绪状态才有机会再次运行。根据其阻塞的原因,可分为三种:

    • a、等待阻塞:线程运行时调用了wait()方法,进入等待阻塞状态。可通过notify()或者notifyAll()唤醒。
    • b、同步阻塞:线程在获取同步锁synchronized失败,(可能被其他线程占用)进入到同步阻塞状态。
    • c、其他阻塞:调用线程中的sleep()方法,或者join()方法,线程进入阻塞状态。当sleep时间到、join ()等待时间终止或者I/O处理完毕,进入就绪状态。
  • 5、死亡状态:线程线程执行完run()方法或者因异常退出而终止,该线程就结束了生命周期。

再来个简单的结构图:

了解了线程的基本形态,那么如何创建多线程呢?

> 多线程的创建

在java中,多线程的创建有三种基本的形式,由于知识有限【尴尬】,在这里只给大家介绍常用的两种基本形式:

  • 1、继承Thread类。
  • 2、实现Runable接口

第一种:继承Thread类,重写run()方法:

代码实例:

  public class ThreadWork1 extends Thread {  //继承Thread类
  private int num = 10;
  @Override
  public void run() {  //重写run()方法
      // TODO Auto-generated method stub
      super.run();
      for (int i = 0; i < 100; i++) {   //用5个线程输出10以内的偶数
          if(num > 0){  
              num--;
              if( num % 2 == 0) {
              System.out.println(Thread.currentThread().getName() + "输出" + num);
              try {
                  Thread.sleep(500);
                } catch (InterruptedException e) {
                  e.printStackTrace();
                }
             }
          }
       }
   }
}

  public class ThreadTest1 {
    //测试类,定义5个线程,并且调用.start()方法启动线程
  public static void main(String[] args) {
      Thread thread1 = new ThreadWork1();
      Thread thread2 = new ThreadWork1();
      Thread thread3 = new ThreadWork1();
      Thread thread4 = new ThreadWork1();
      Thread thread5 = new ThreadWork1();
      thread1.start();
      thread2.start();
      thread3.start();
      thread4.start();
      thread5.start();
   }
}

输出结果:

第二种:实现Runable接口,重写run()方法。

代码实例:

    public class ThreadDemo implements Runnable{  //实现Runable接口
    private int ticket = 5;
    @Override
    public void run() {  //重写run方法,用四个线程,模拟卖票
        // TODO Auto-generated method stub
        for(int i=0;i<100;i++){
            if(ticket>0){
                System.out.println(Thread.currentThread().getName()+"我正在出售第"+(ticket--)+"张票");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }   
    }
}

    public class ThreadTest {  //测试类,定义四个线程并给线程起名字为"窗口X",并启动线程

    public static void main(String[] args) {
        ThreadDemo4 t4 = new ThreadDemo4();
        Thread thread1 = new Thread(t4,"窗口一:");
        Thread thread2 = new Thread(t4,"窗口二: ");
        Thread thread3 = new Thread(t4,"窗口三: ");
        Thread thread4 = new Thread(t4,"窗口四 :");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}       

输出结果:

根据输出的结果发现,窗口一和窗口三都在出售第四张票,按照实际情况来说,是不会出现这样的情况的(就比如坐火车,买了同样的一张票该怎么办,到时候就必定引起矛盾了,然后。。。扯远了)所以我们就引入了线程的同步问题。线程同步问题等下再说,先看这两种创建线程的形式,继承和实现接口有什么区别呢?

不同的地方在于创建线程时的不同:

继承---> Thread thread1 = new ThreadWork1();

实现接口---> ThreadDemo4 t4 = new ThreadDemo4();
            Thread thread1 = new Thread(t4);

这两种方法都可以成功创建线程,具体需要用什么形呢?如果一个类已经继承了别的父类,那就得需要用到实现Runable接口来创建线程了。

线程的安全性问题

我们刚才说到模拟卖票的实例,有两个窗口同时卖了同一张票,甚至多次运行还会出现0和负数,那么遇到这种问题该如何解决呢?

这就需要引入多线程的同步(synchronized)

    public void run() {
       while(true){
        synchronized (this) {//通常将当前对象作为同步对象
            if (tick>0) {
              Thread.sleep(10);
                System.out.println(Thread.currentThread().getName()+"卖票:"+tick--);
            }       
         }  
      }
    }

在run()方法中使用了synchronized(this){}同步代码块,一般来说,同步代码块会放在需要同步数据的位置,也可以放在方法中,让方法实现同步。

  public  void run() {
      while(true){
       sale();
     }
 }
public synchronized void sale(){ //同步放在方法中
    if (tick>0) {
        Thread.sleep(10);
         System.out.println(Thread.currentThread().getName()+"卖票:"+tick--);
    }
}

同步监视器:
* synchronized(obj){}中的obj称为同步监视器
* 同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器
* 同步方法中无需指定同步监视器,因为同步方法的监视器是this,也就是该对象本身

同步监视器的执行过程:

  • 第一个线程访问,锁定同步监视器,执行其中代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器未锁,锁定并访问

简单的来说,同步就是让一个线程操作共享的数据,其他线程等待,该线程执行完另一个线程开始执行。

同步的前提:

  • 必须有两个或者两个以上的线程。
  • 必须是多个线程使用同一个资源。
  • 必须保证同步中只能有一个线程在运行。

同步可以保证资源共享操作的正确性,但过多的同步会导致死锁问题。

死锁一般是线程在互相等待,都没有执行。解决方法,引入了java多线程的通讯。

线程的通讯

需要用到消费者和生产者问题:

/**
* 实体类
* @author lt
*
*/
public class Q {
private String name;
private int num;
boolean value = false;
/**
 * 消费
 * @return
 */
public synchronized String get() {
    if(!value){
        try {
            wait();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    System.out.println("消费第"+num+"瓶"+name);
    System.out.println("------------");
    value = false;
    notify();
    return num+name;
}

/**
 * 生产
 * @param name
 * @param num
 */
public synchronized void set(String name,int num) {
    if(value){
        try {
            wait();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    this.name = name;
    this.num = num;
    value = true;
    System.out.println("生产第"+num+"瓶"+name);
    notify();
    }

}

.定义实体类,定义属性及消费方法,生产方法。

/**
* 生产者类
* @author lt
*
*/
public class Producer implements Runnable{
    Q q = new Q();
    Thread thread;
    public Producer(Q q) {
        // TODO Auto-generated constructor stub
        this.q = q;
        thread = new Thread(this,"生产");
        thread.start();
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        int num = 1;
        while(true){
            q.set("娃哈哈", num++);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

.定义生产者类,实现接口,重写方法。

/**
* 消费者类
* @author lt
*
*/
public class Consumer implements Runnable{
    Q q = new Q();
    Thread thread;
    public Consumer(Q q) {
        // TODO Auto-generated constructor stub
        this.q = q;
        thread = new Thread(this,"消费");
        thread.start();
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        while(true){
            q.get();
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

.定义消费者类,同样实现接口,重写方法。

    public class AppMain {
    //测试类
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Q q = new Q();
        new Producer(q);
        new Consumer(q);
    }
}

运行结果:

由结果发现,生产一瓶,消费一瓶,这样保证了资源的正确性,也不会存在线程死锁问题。在实体类中,定义了一个boolean类型的变量模拟死锁问题,运用wait()方法和notify()方法,让线程停止然后另一个线程执行完毕后再唤醒,有效的解决的因资源同步出现的死锁问题。

——————————————————————————————————————————

本人知识有限,如有错误或者不准确的地方,感谢指正【抱拳】。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值