欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java学习笔记 线程

程序员文章站 2022-03-08 18:59:10
...

一,线程概述
1.进程:正在运行的程序,负责了这个程序的内存空间分配,代表了内存中的执行区域
2.线程:在一个进程中负责一个执行路径
3.多线程:在一个进程中多个执行路径同时执行
4.多线程好处:解决了一个进程里面可以同时运行多个任务;提供资源的利用率而不是提供效率
5.多线程弊端:降低了一个进程里面的线程的执行频率;对线程进行管理要求额外的CPU开销;共有变量的同时读或写;线程的死锁

二,创建线程方式,常用的有两种
1.继承Thread类

/*
* 1.定义Thread类的子类,并重写该类的run方法
* 2.创建Thread子类的实例,即创建了线程对象
* 3.调用线程对象的start方法启动该线程
*/
public class FirstThread extends Thread{
     private int i;
     public void run(){
         for(;i<100;i++){
              System.out.println(getName()+" "+i);
         }
     }
     public static void main(String [] args){
         for(int i=0;i<100;i++){
              System.out.println(Thread.currentThread().getName()+" "+i);
              if(i==20){
                  new FirstThread().start();
                  new FirstThread().start();
              }
         }
     }
}

输出部分截图
Thread-1 98
Thread-1 99
Thread-0 39
Thread-0 40
Thread-0 41
Thread-0 42
Thread-0 43
Thread-0 44
Thread-0 45
Thread-0 46
Thread-0 47
Thread-0 48
Thread-0 49
Thread-0 50
Thread-0 51
Thread-0 52
Thread-0 53
main 62
main 63
main 64

2.实现Runnable接口

/*
* 1.定义Runnable接口的实现类,并重写该接口的run方法
* 2.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象
* 3.调用线程对象的start方法启动该线程
*/
public class SecondThread implements Runnable{
     private int i;
     public void run(){
         for(;i<100;i++){
              System.out.println(Thread.currentThread().getName()+" "+i);
         }
     }
     public static void main(String [] args){
         for(int i=0;i<100;i++){
              System.out.println(Thread.currentThread().getName()+" "+i);
              if(i==20){
                  SecondThread st=new SecondThread();
                  new Thread(st,"新线程1").start();
                  new Thread(st,"新线程2").start();
              }
         }
     }
}

输出部分截图
新线程2 91
新线程1 90
新线程2 92
新线程2 94
main 72
新线程2 95
新线程1 94
新线程1 97
main 73
新线程1 98
新线程2 97
新线程1 99

由以上结果可得出:第一种方法,继承Thread类创建线程,多个线程之间无法共享线程类的实例变量;第二种方法,多个线程可以共享同一个线程类的实例变量

三,线程的状态
java学习笔记 线程

1.新建:程序用new关键字创建了一个线程, 由java虚拟机为其分配内存并初始化成员变量
2.就绪:线程对象调用start()方法,java虚拟机为其创建方法调用栈和程序计数器,表示该线程可以运行了,何时运行取决于JVM线程调度器的调度
3.运行:获取CPU执行权,开始执行run方法的线程执行体
4.阻塞:线程因为某种原因放弃CPU使用权,暂停停止运行,直到线程进入就绪状态才有机会转到运行状态
5.死亡:线程执行完它的任务,测试某个线程是否已经死亡,调用isAlive()方法,就绪、运行、阻塞三种状态返回true,新建、死亡返回false

四,锁对象
每个java对象都有一个锁对象.而且只有一把钥匙.
如何创建锁对象:
可以使用this关键字作为锁对象,也可以使用所在类的字节码文件对应的Class对象作为锁对象
类名.class
对象.getClass()

Java中的每个对象都有一个内置锁,只有当对象具有同步方法代码时,内置锁才会起作用,当进入一个同步的非静态方法时,就会自动获得与类的当前实例(this)相关的锁,该类的代码就是正在执行的代码。获得一个对象的锁也成为获取锁、锁定对象也可以称之为监视器来指我们正在获取的锁对象。
因为一个对象只有一个锁,所有如果一个线程获得了这个锁,其他线程就不能获得了,直到这个线程释放(或者返回)锁。也就是说在锁释放之前,任何其他线程都不能进入同步代码(不可以进入该对象的任何同步方法)。释放锁指的是持有该锁的线程退出同步方法,此时,其他线程可以进入该对象上的同步方法。

1:只能同步方法(代码块),不能同步变量或者类
2:每个对象只有一个锁
3:不必同步类中的所有方法,类可以同时具有同步方法和非同步方法
4:如果两个线程要执行一个类中的一个同步方法,并且他们使用的是了类的同一个实例(对象)来调用方法,那么一次只有一个线程能够执行该方法,另一个线程需要等待,直到第一个线程完成方法调用,总结就是:一个线程获得了对象的锁,其他线程不可以进入该对象的同步方法。
5:如果类同时具有同步方法和非同步方法,那么多个线程仍然可以访问该类的非同步方法。
同步会影响性能(甚至死锁),优先考虑同步代码块。
6:如果线程进入sleep() 睡眠状态,该线程会继续持有锁,不会释放。

死锁问题:当多个线程完成功能需要同时获取多个共享资源的时候就可能会导致死锁,如果一个类可能发生死锁,那么并不意味着每次都会发生死锁,只是表示有可能,要避免程序中出现死锁。

五,线程间的通信
线程间通信其实就是多个线程在操作同一个资源,但操作动作不同。例如:有一个数据存储空间,划分为两个部分,一部分存储人的姓名,一部分存储性别,我们开启一个线程,不停地想其中存储姓名和性别(生产者),开启另一个线程从数据存储空间中取出数据(消费者)。
由于是多线程的,就需要考虑,假如生产者刚向数据存储空间中添加了一个人名,还没有来得及添加性别,cpu就切换到了消费者的线程,消费者就会将这个人的姓名和上一个人的性别进行了输出。
还有一种情况是生产者生产了若干次数据,消费者才开始取数据,或者消费者取出数据后,没有等到消费者放入新的数据,消费者又重复的取出自己已经取过的数据。

package Thread;

public class ThreadTest {

    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();
    }

}

class Person{
    String name;
    String gender;
}

class Producer implements Runnable{
    Person p;
    public Producer(){

    }

    public Producer(Person p){
        this.p=p;
    }

    public void run(){
        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(){

    }

    public Consumer(Person p){
        this.p=p;
    }

    public void run(){
        while(true){
            System.out.println("name:"+p.name+"--gender:"+p.gender);
        }
    }
}
输出部分截图
name:小丽--gender:女
name:jack--gender:man
name:jack--gender:女
name:jack--gender:man
name:小丽--gender:man
name:jack--gender:man
name:jack--gender:man
name:小丽--gender:man
name:jack--gender:man
name:jack--gender:man
name:小丽--gender:man

由输出可知,会出现Jack是女这样的线程安全问题,使用synchronized来解决,每个run 方法的方法体用synchronized来标识,代码略。
但是运行过后会发现,还是存在问题,生产者生产了若干次数据,消费者才开始取数据,或者消费者取出数据后,没有等到消费者放入新的数据,消费者又重复取出自己已经取过的数据。

改进:在Person类中添加另个方法,set和read方法并设置为synchronized,让生产者和消费者调用者两个方法。同时,设置一个标记,表示数据的存储空间的状态。例如,当消费者读取了(消费了一次)一次数据之后可以将标记改为false,当生产者生产了一个数据,将标记改为true。也就是只有标记为true的时候,消费者才能取走数据,标记为false时候生产者才生产数据。

package Thread;

public class ThreadTest {

    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();
    }

}

class Person{
    String name;
    String gender;
    boolean flag=false;

    public synchronized void set(String name, String gender){
        if(flag){
            try{
                wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        this.name=name;
        this.gender=gender;
        flag=true;
        notify();
    }

    public synchronized void read(){
        if(!flag){
            try{
                wait();
            }catch(InterruptedException e){
                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;
    }

    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;
    }

    public void run(){
        while(true){
            p.read();
//          System.out.println("name:"+p.name+"--gender:"+p.gender);
        }
    }
}
输出结果部分截图
name:小丽--gender:女
name:jack--gender:man
name:小丽--gender:女
name:jack--gender:man
name:小丽--gender:女
name:jack--gender:man
name:小丽--gender:女
name:jack--gender:man
name:小丽--gender:女
name:jack--gender:man
name:小丽--gender:女
name:jack--gender:man

线程间通信方法
wait:告诉当前线程放弃执行权,并放弃锁进入阻塞状态,直到其他线程获得执行权,并持有相同的锁调用notify为止
notify:唤醒持有同一个锁中调用wait的第一个线程,被唤醒的线程是进入了可运行状态。eg,餐馆有空位置后,等待就餐最久的顾客最先入座
notifyAll:唤醒持有同一个监视器中调用wait的所有线程

六,补充
1.常见线程方法
Thread(String name)初始化线程的名字
getName()返回线程的名字
setName(String name)设置线程对象名
sleep()线程睡眠指定的毫秒数。
getPriority() 返回当前线程对象的优先级,默认线程的优先级是5
setPriority(int newPriority) 设置线程的优先级,虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的实现(最大的优先级是10 ,最小的1 , 默认是5)。
currentThread()返回CPU正在执行的线程的对象
join:当某个程序执行流中调用其他线程的join方法,调用线程将被阻塞,直到被join方法加入的join线程执行完为止
yield:让当前正在执行的线程暂停,但不会阻塞,转入就绪状态。当某个线程调用了yield,只有优先级与当前线程相同或者比当前线程更高的处于就绪状态的线程才会获得执行的机会
2.后台线程
后台运行,为其他线程提供服务,JVM的垃圾回收线程就是典型的后台线程;如果所有前台线程都死亡,后台线程会自动死亡,实现setDaemon(true)