面试准备:java Thread详解

Java Thread总结

  1. 在操作系统里面,进程是作为资源分配的基本单位,而线程是作为独立运行和调度的基本单位;
  2. 引入线程的目是能让程序并发执行,尽可能地提高CPU的利用率;java种一般会将一些耗时的操作,譬如说访问网络操作啊,文件读写操作、IO操作啊放到线程中取执行;
  3. Java里面的Thread有两种实现方式,一种是继承Thread类,重写run方法,一种是实现Runnable接口,进而实现run方法;第二种实现方式,也就是实现Runnable接口的方式它只是创建了一个线程体而已,而并没有创建一个新的线程对象,还要把实现了Runnable接口的类作为参数传给一个线程对象。
  4. 在实际开发中创建线程用得比较多方式是第二种,主要是因为Java的话是单继承,如果一个类已经继承了某个类的话就不能再去继承这个Thread类了,还有一个就是有利于共享创建的这个线程体资源;
  5. 启动线程的方式是只能调用start而不要自己调用run方法,原因是因为start方法会为线程的执行分配系统资源、其次也会调用run方法;
  6. 线程停止的话不能通过调用stop方法来停止,而是让run方法执行完毕,让线程自然结束,又或者线程执行过程中是抛出了异常;
  7. 多线程并发执行访问某一临界资源的话如果不加以控制很容易导致多线程的运行结果与我们预期的结果不相符,因此引入了线程同步机制来控制线程的并发执行。Java中提供了synchronized关键字来实现线程的同步机制,实现的方式呢是给对象上锁:java中每一个对象都有一把锁(lock)或者叫监视器(monitor),当线程访问某个对象的synchronized方法或者synchronized块的时候,就会为该synchronized方法或者synchronized块所在的对象上锁,这时其它线程都无法访问这个加了锁的对象的synchronized方法或者synchronized块,只有在第一个线程执行完毕或者抛出了异常,将该对象的把锁释放掉,其它线程才可以访问;
  8. 但是使用synchronized关键字仅仅是实现了一个线程在访问某一临界资源的时候,其它线程无法访问这样一个简单的协调,但是如果要涉及到多线程之间的通信的话就得依靠Object中提供的wait,notify、notifyAll这些方法了。调用这些方法的话有一下要注意的地方:
    1、调用wait,notify、notifyAll的前提呢是当前线程要拥有对象的锁,换句话说也就是这些方法应该在synchronized方法或者synchronized块里面被调用;
    2、调用了wait方法就相当于释放了对锁的拥有权,线程停止执行并且开始等待,等到获得了对象的锁之后才能继续执行;
    3、notify方法唤醒等待对象锁的线程,如果有多个的线程都在等待这个对象,则会随机地唤醒一个wait线程;
    4、还有一个就是wait方法和notify方法会在同一个对象中成对出现。
    线程与进程

  9. 概念
    1、线程:操作系统中作为资源分配的基本单位;
    2、进程:操作系统中作为独立运行和调度的基本单位;一个进程可能包含多个线程,且线程能够并发执行。
    3、程序:程序是静态的,进程是运行中的程序;一个程序可能包含多个进程

  10. 引入线程的目的:使多个程序能够并发执行,以提高CPU的利用率和系统吞吐量。
  11. 线程与进程的区别:
    1、概念区别:资源分配的基本单位 + 独立运行和CPU调度的基本单位;
    2、进程有自己独立的内存空间 + 同一个进程中的线程共享内存资源
    3、线程本身的数据通常只有寄存器数据和少量的堆栈数据,所以线程的切换代价比进程少。

引入线程的目的:最大限度利用CPU资源
Main方法就是一个主线程。

线程的两种实现方式
线程:并发、优先级、名字(线程可能会有相同的名字,名字可以自己指定,也可以用用系统自带的Thread-自增的Num)、启动线程的唯一方法(start(),start()会调用run方法)、线程只能被启动一次(也不能重新启动)、将要并发执行的代码放到run方法中、通过start方法启动线程(start方法首先会为要执行Thread分配系统资源,然后才调用run方法)。

java中线程的两种实现方式:继承Thread类,重写run方法 + 实现runnable接口,进而实现run方法(实现runnable接口的类作为参数传给Thread对象)。

  1. 继承Thread类,重写run方法
    1、实现的步骤:继承Thread类 + 重写run方法(将要并发执行的代码放到run方法中) + 通过start方法启动线程(start方法一方面会为Thread分配内存资源、另一方面会调用run方法)
package com.dnegqi.thread;
public class ThreadTest1
{
    public static void main(String[] args)
    {
        Thread t1 = new Thread1();
        Thread t2 = new Thread2();
        t1.start();
        t2.start();
    }
}
class Thread1 extends Thread {
    @Override
    public void run()
    {
            for(int i = 0; i < 30; i++) {
                System.out.println("Thread一"+ i + " ");
            }           
    }   
}
class Thread2 extends Thread {
    @Override
    public void run()
    {
            for(int i = 0; i < 30; i++) {
                System.out.println("Thread二" + i + " ");
            }           
    }   
}

2、某个类继承了Thread类,这个类就是一个线程类。

  1. 实现Runnable接口进而实现run方法
    1、Runnable接口中只有一个run方法
    2、实现步骤:实现Runnable接口 + 实现run方法 + 将实现Runnable接口的类作为参数传给一个Thread类 + 调用start方法启动线程。
package com.dnegqi.thread;

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

        Thread t1 = new Thread(new MyThread());
        t1.start();
    }
}

class MyThread implements Runnable {

    @Override
    public void run()
    {
        // TODO Auto-generated method stub
        for(int i = 0; i < 20; i ++) {
            System.out.println("Thread一" + i);
        }
    }
}

线程两种实现方式的关系
第一种方式:
1. Thread类也实现了Runnable接口
(看一下两种实现方式的构造方法)
2. new Tread()构造方法的实现代码:
1、

//第一种实现方式的构造方法只是调用的init()方法,参数为:
init(null, null, "Thread-" + nextThreadNum(), 0);

该构造方法可以为线程制定名字:Thread-0++,但是也可以通过new Thread(String threadName)的方法制定名字
第二种方式
3.new Thread(new Runnable())调用的也是init()方法,只不过奖new Runnable对象传给了init方法的第二个参数target。


//第二种实现方式的构造方法调用的init()方法参数为:
//其中target参数为传进来的实现了Runnable接口的类
init(null, target, "Thread-" + nextThreadNum(), 0);
//nextThreadNum()返回threadInitNumber++,没了
  1. 当使用第一种方式来生成线程对象的时候,我们要重写run方法,因为Thread类的run方法此时什么事情也不会做;
  2. 使用第二种方式生成线程对象的时候,我们要实现Runnable接口的run方法,然后使用new Thread(new Runnable())来生成线程对象,这是的线程对象run方法就会调用实现Runnable接口中的run方法
    即:
//Thread类中的run方法
Void run(Runnabke target) {
    if(target != null) {
    {
        target.run();
    }

两种实现方式的细节知识

  1. 两种实现方法启动线程的方式都是要通过调用start方法来启动的,start方法一方面会为线程分配系统资源、另外一个是调用重写或者实现的run方法
  2. 在具体应用中,选择哪种方式来实现线程依据情况而定,我们知道java是只支持单继承的,所以一个类已经继承了另一个类的话,就只能实现Runnable接口了。
  3. 停止线程:
    线程的消亡不能通过调用stop方法,而是让run方法自然结束,可以当Thread的 这个stop方法不存在,要让它停止的话可以用如下两个方法来控制:
    1、通过while循环结合flag变量控制:
Boolean flag = true;
run(){
    while(flag){
        //执行完之后设置flag的值为false;退出循环。
    }
}

2、在run方法的While循环中使用break。

线程的几种状态(生命周期)
创建状态 + 就绪状态 + 运行状态 + 阻塞状态 + 消亡状态
这里写图片描述

  1. 创建状态:通过new关键字创建的线程对象再没有start之前只是一个空的线程对象,系统还没有为它分配资源
  2. 可运行状态:调用了start方法,为线程分配了相应的系统资源,并调用线程的run方法;此时的线程只是出于可运行状态还没有真正地运行,要抢占到CPU才能算是真正的运行
  3. 不可运行状态(Blocked):sleep方法 + wait等待 + 阻塞(IO)
  4. 返回可运行的状态:Sleep了指定的时间 + notify或notifyAll + IO完成
  5. 消亡状态:run方法执行完毕或者抛出了异常,自然消亡

线程的优先级(了解下即可)

  1. 线程优先级的设置遵循的原则:
    1、线程创建时,子类继承父类的优先级
    2、可以通过setPrority改变线程的优先级
    3、优先级为1–10的整数
    (线程的优先级是动态的,譬如说随着等待的时间越长、优先级应该动态增加)

线程的调度策略
线程终止运行的几种情况
1. 线程体调用了yeild方法让出了CPU的使用权,让其它线程执行
2. sleep方法
3. IO阻塞
4. 遇到优先级更高的抢占了CPU
5. 时间片完

多线程的同步

  1. 线程共享类成员变量,而不共享局部变量(主要是指Runnable接口)
    例如如下代码的执行次数为10次,而把i变成局部变量之后,执行的次数为20次。
    总的来说就是:多个线程共享一份成员变量,但是对局部变量的话都有自己的一份拷贝。
package com.dnegqi.thread;

public class ThreadTest2
{
    public static void main(String[] args)
    {
        Runnable  r1 =  new HelloThread();
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r1);
        t1.start();
        t2.start();
    }
}
class HelloThread implements Runnable { 
//  private int i = 0;
    public void run() {
            int i = 0;//这里把i变成局部变量
        while(true) {
            System.out.println("number: " + i++);
            if(10 == i) {
                break;
            }
        }
    }
}

多线程的同步问题

  1. 为什么要引入同步机制
    在多线程环境中,可能会有多个线程试图访问同一个有限资源,
    解决方法:对该资源加锁,加了锁之后,其它线程就不能访问了。
    例如两个线程分别从Bank中取钱的例子:
package com.dnegqi.thread;

public class FetchMoney
{
    public static void main(String[] args)
    {
        Bank bank = new Bank();
        Thread t1 = new MoneyThread(bank);
        Thread t2 = new MoneyThread(bank);
        t1.start();
        t2.start();
    }
}

class Bank {
    private int money = 1000;
    //改进为:为该方法添加synchronized关键字
    public (synchronized)int getMoney(int number) throws Exception {
        if(number < 0) {
            return  -1;
        }
        else if(number > money) {
            return -2;
        }else if(money < 0) {
            return -3;
        }
        else {
            //模拟取钱的耗时
            Thread.sleep(1000);         
            money = money - number;
            return number;
        }
    }
}


class MoneyThread extends Thread {
    private Bank bank;

    public MoneyThread(Bank bank){
        this.bank = bank;
    }

    @Override
    public void run()
    {
        // TODO Auto-generated method stub
        super.run();
        try
        {
            System.out.println(bank.getMoney(800));
        }
        catch (Exception e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}
  1. Synchronized关键字(这儿非常重要)
    当synchronized关键字修饰一个方法的时候,这个方法叫同步方法
    功能:
    1、给对象上锁:当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。java中每一个对象都有一把锁(lock)或者叫监视器(monitor),当线程访问某个对象的synchronized方法的时候,就会为该synchronized方法所在的对象上锁,这时其它线程都无法访问这个synchronized方法,只有在第一个线程执行完毕或者抛出了异常,将该对象的把锁释放掉,其它线程才可以访问

2、是给该synchronized方法所在的对象上锁
如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到某个synchronized方法,那么在该方法没有执行完毕之前,其它线程都无法访问这个对象的任何synchronized。
例如,一下程序顺序执行:

package com.dnegqi.thread;

public class ThreadTest3{
    public static void main(String[] args){
        Example example = new Example();
        Thread t1 = new TheThread(example);
        Thread t2 = new TheThread2(example);
        t1.start();
        t2.start();
    }
}

class Example {
//假如这里添加了Static关键字的话
    public synchronized  void execute(){
        for(int i = 0; i < 10; i++) {
            try{
                Thread.sleep(500);
            }
            catch (InterruptedException e){
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("dengqi" + i);
        }
    }

    public synchronized void execute2(){
        for(int i = 0; i < 10; i++) {
            try{
                Thread.sleep(500);
            }
            catch (InterruptedException e){
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("GG" + i);
        }
    }

}

class TheThread extends Thread {
    private Example example;
    public  TheThread(Example example) {
        this.example = example;
    }

    @Override
    public void run()
    {
        // TODO Auto-generated method stub
        super.run();
        example.execute();
    }
}

class TheThread2 extends Thread {
    private Example example;

    public  TheThread2(Example example) {
        this.example = example;
    }

    @Override
    public void run()
    {
        // TODO Auto-generated method stub
        super.run();
        example.execute2();
    }
}

//假如这里添加了Static关键字的话public synchronized void execute(),就表示锁的不是该对象,而是该类
即如果 某个synchronized方法是static,那么当线程访问该方法时,它锁的不是synchronized方法所在的对象,而是synchronized方法所在对象所对应的Class类,因为java中无论一个类有多少个对象,这些对象会对应唯一的一个Class类,因此当线程分别访问同一个类的两个对象的两个static synchronized方法是,他们执行顺序也是有序的,也就是说一个线程先去执行该synchronized方法,执行完毕之后才释放锁让另外的线程执行。

使用synchronized代码块实现同步
锁的是一个对象,任何对象都行,表示synchronized(对象)代码块中的对象给锁上.(Object并没有其它实际以及,而只是起到锁的作用)
例如:

package com.dnegqi.thread;

public class ThreadTest4
{   
    public static void main(String[] args)
    {
        Example2 e = new Example2();

        TheThread3 t1 = new TheThread3(e);
        TheThread4 t2 = new TheThread4(e);
        t1.start();
        t2.start();
    }
}
class Example2 {
    private Object object = new Object();

    public  void execute(){
        synchronized (object)
        {
            for(int i = 0; i < 10; i++) {
                try
                {
                    Thread.sleep(500);
                }
                catch (InterruptedException e)
                {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println("dengqi" + i);
            }
        }
    }

    public  void execute2(){

        synchronized (object)
        {       
            for(int i = 0; i < 10; i++) {
            try{
                Thread.sleep(500);
            }
            catch (InterruptedException e){
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("GG" + i);
        }   
    }   
}

class TheThread3 extends Thread {
    private Example2 example;

    public  TheThread3(Example2 example) {
        this.example = example;
    }

    @Override
    public void run(){
        // TODO Auto-generated method stub
        super.run();
        example.execute();
    }
}

class TheThread4 extends Thread {
    private Example2 example;

    public  TheThread4(Example2 example) {
        this.example = example;
    }   

    @Override
    public void run()
    {
        // TODO Auto-generated method stub
        super.run();
        example.execute2();
    }
}

如果锁的是不同对象,结果还是乱序的。如果要使用synchronized块实现synchronized方法同样的效果话就可以这样来synchronized(this)

Synchronized块的话更灵活、更细腻。如果一个方法有很多代码的时候,使用Synchronized方法的话会把整个方法给锁住。
即:Synchronized方法中粗粒度的并发控制,某一时刻只能有一个线程执行该Synchronized方法;Synchronized块则是中细粒度的并发控制,只会将块中代码同步,位于方法内、Synchronized之外的代码是可以被多个线程同时访问的。

引入同步机制来控制线程的并发,不然多个线程访问某个临界资源的时候很容易出错;但是仅有这样是不行的,试想一下,假如有很多个线程都要访问一个对象中的synchronized方法,而且耗时2秒,那么就要等这个线程过了2秒执行结束之后,才能轮到下一个线程执行,那什么时候才轮到第10个呢,这几要求不能让线程盲等。

jdk1.5java.util.current:机制就是用户不是忙等,假如等了10秒钟还是没响应,就给用户返回一个响应的提示信息。

添加synchronized之后线程转换图(这还不是最终的情况):
这里写图片描述

之前的情况就是:线程启动之后就没办法控制了,synchronized方法仅仅是实现了一个线程在访问某一资源的时候,另外一个线程无法访问这样一个简单的协调,但是并没有实现线程之间的通信,譬如说我这边干完了,通知你你可以开始干了。

例如:生产者(线程)、消费者(线程)问题
生产者生产了产品之后告诉消费者可以消费了,消费者消费完之后告诉生产者可以生产了
又譬如:哲学家进餐问题

例如:类中两个方法,方法1实现成员变量+1,方法2实现成员变量-1;有两个线程类,第一个线程类调用方法1,第二个线程类调用方法2,;之后启动四个线程(两加两减),要求输出的结果始终是010101,这几必须得涉及到线程与线程之间的协调

Object中的wait、notify、notifyAll方法
public final void wait()方法,(final不能被重写(继承),但是可以有重载):会使得当前线程去等待,直到另一个线程调用了当前对象的notify或者notifyAll。当前的线程(调用wait方法的线程)必须要拥有对象的锁,也就是说当前线程应该再synchronized方法或者synchronized块里面,这样才能保证自己获得了锁。换句话说wait方法一定是在synchronized方法或者synchronized块里面;
调用了wait方法就相当于释放了对锁的拥有权,停止执行并且开始等待,直到另外的线程通过notify或者notifyAll方法通知等待对象锁的线程唤醒,唤醒后重新获得对象锁的拥有权之后才会继续执行
即:wait方法的一个重要特点:wait方法只能在拥有对象锁的线程中调用。
public final void notify():唤醒等待对象锁的线程,如果有多个的线程都在等待这个对象,则会随机地唤醒一个wait线程;被唤醒的线程还不会执行,直到当前的线程放弃了对象的锁。唤醒的线程之间会有竞争,它们竞争的是谁来获得对象的锁,然后开始执行。
notify也是只能被拥有对象锁的Thread调用,即notify也应该是在synchronized方法或者synchronized块中被调用。一个Thread成为对象锁的拥有者的三种方法:
1、通过执行synchronized的实例方法
2.通过还行synchronized语句
3、对于Class类,执行这个类对应的static方法

wait和notify一定是在同一个对象中成对出现的

例如:实现0/1的交替打印:这里判断线程是否调用wait方法释放锁要注意:
1、未满足特定的条件
2、满足条件后由于notify是随机的通知线程获得锁,因此要使用while循环判断线程是否能继续执行。

package com.dnegqi.thread2;
public class Sample
{
    private int num;
    public synchronized void increase()
    {
        while (0 != num)
        {
            try
            {
                wait();
            }
            catch (InterruptedException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        System.out.println("dengqi");
        num++;
        System.out.println(num);
        notify();
    }
    public synchronized void decrease()
    {
        while (0 == num)
        {
            try
            {
                wait();
            }
            catch (InterruptedException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            num--;
            System.out.println(num);
            notify();
        }
    }
}

总结下wait和notify的使用方式:
wait和notify方法都是定义在Object类中,而且是final的,因此会被所有java类所继承并且无法重写;
这两个方法要求在调用时线程已经获得了对象的锁,因此这两个方法的调用需要放在synchronized方法或synchronized块中
当线程执行了wait方法是,它会释放掉对象的锁;而sleep不会释放掉任何锁的拥有权。

最后,线程一个最完整的状态转换图:
这里写图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值