Java 多线程基础

ⅰ亾dé卋堺 提交于 2020-01-07 13:18:39

Java 多线程基础

Java使用多线程进行并发编程。

一、进程、线程、协程、守护线程

进程: 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程: 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

协程: 协同线程,某个线程为主线程,其他线程辅助它。主线程结束,辅助线程直接结束。主线程存活,其他线程才可以执行任务,类似于java中的守护线程。

守护线程: Java使用多线程并发,也有守护线程的辅助。

状态: 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

优先级:线程之间也有优先级,优先级高的先执行(并非绝对)。

线程竞争资源,执行结果不确定,优先级也不一定靠谱,每次执行由cpu调度资源。

可以通过加锁进行人为控制。

二、Java创建线程

Java创建线程的方法有三种,一种是继承Thread类,另外一种是实现Runable接口,最后一种不谈。

1、继承Thread类创建线程

重写run()方法、使用start()方法开启线程。

//继承Thread类实现多线程
public class ThreadDemo extends Thread {

    public void run()   {
        System.out.println("Thread "+Thread.currentThread().getName()+"执行");
    }
    public static void main(String[] args) throws Exception{
        ThreadDemo i=new ThreadDemo();
        i.start();
    }

}

2、实现Runnable接口实现多线程

Runnable接口是一个函数式接口,只有一个run()方法,Thread类也是实现了该接口。

重写run方法后,借助Thread类开启线程。

//实现Runnable接口实现多线程
public class RunDemo implements Runnable{

    public void run() {//实现run方法
        System.out.println("Thread "+Thread.currentThread().getName()+"执行");

    }

    public static void main(String[] args) {
        RunDemo r=new RunDemo();
        Thread a = new Thread(r,"线程a");//构造函数,传入一个Runnable对象,以及线程名
        Thread b = new Thread(r,"线程b");
        Thread c = new Thread(r,"线程c");
        a.start();
        b.start();
        c.start();
    }

两种方式的比较

继承Thread类有单继承局限,不能操作同一个对象,无法共享内存。

实现Runable接口,对同一个对象可以开启多个线程,建议采用。

3、Thread类的组成

Runnable接口没啥好说的,主要看Thread类。

构造方法

Thread() 
         
Thread(Runnable target) 
         
Thread(Runnable target, String name) 
       
Thread(String name) 
          
Thread(ThreadGroup group, Runnable target) 
         
Thread(ThreadGroup group, Runnable target, String name) 
         
Thread(ThreadGroup group, Runnable target, String name, long stackSize) 
         
Thread(ThreadGroup group, String name) 
        

静态方法

public static Thread currentThread()返回对当前正在执行的线程对象的引用。 

public static void yield()暂停当前正在执行的线程对象,并执行其他线程。 

public static void sleep(long millis) throws InterruptedException
在指定的毫秒数内让当前正在执行的线程休眠

public static int activeCount(): 程序中活跃的线程数。 

方法

    
   isAlive(): 判断一个线程是否存活。

  join(): 当前线程等待使用该方法的线程终止。 

  enumerate(): 枚举程序中的线程。 

  isDaemon(): 一个线程是否为守护线程。 

  setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) 

  setName(): 为线程设置一个名称 
  getName():得到线程名称

  wait(): 强迫一个线程等待。 

  notify(): 通知一个线程继续运行。 

  setPriority(): 设置一个线程的优先级

Thread类的toString()方法

"Thread[" + getName() + "," + getPriority() + "," +
                          group.getName() + "]";

4、方法解释

wait和notify方法与锁有关,在后文解释。

Demo1

以第一个程序为例:

线程信息获取:

public class ThreadDemo extends Thread {

    public ThreadDemo(String name) {
        super(name);//继承Thread类的构造方法,该构造方法参数为线程名字
    }
    public void run()   {
        System.out.println("hello World");
    }
    public static void main(String[] args) throws Exception{
        ThreadDemo i=new ThreadDemo("线程A");//有线程名的构造方法
        i.start();

        System.out.println("name:"+i.getName());//线程名
        System.out.println("id:"+i.getId());//线程id
        System.out.println("state:"+i.getState());//线程状态
        System.out.println("Priority:"+i.getPriority());//线程优先级

        System.out.println("isAlive:"+i.isAlive());//线程是否存活


        System.out.println("currentThread:"+Thread.currentThread());//当前的线程信息
        System.out.println("activeCount:"+Thread.activeCount());//当前存活线程数

    }

}


//执行结果1
name:线程A
hello World
id:11
state:TERMINATED
Priority:5
isAlive:false
currentThread:Thread[main,5,main]
activeCount:2
    
//执行结果2
name:线程A
id:11
state:RUNNABLE
Priority:5
hello World
isAlive:true
currentThread:Thread[main,5,main]
activeCount:2

//执行结果3 
name:线程A
id:11
state:RUNNABLE
Priority:5
isAlive:true
currentThread:Thread[main,5,main]
activeCount:3
hello World

多线程执行结果不确定

三次执行结果不一样,如果再执行,结果不确定,由Cpu调度。

Demo2

以第二个程序为例:

使用 Thread.sleep(100);让进入run方法的线程休眠一会儿

//实现Runnable接口实现多线程
public class RunDemo implements Runnable{

    public void run() {//实现run方法
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread "+Thread.currentThread().getName()+"执行");

    }


    public static void main(String[] args) throws InterruptedException {
        RunDemo r=new RunDemo();
        Thread a = new Thread(r,"线程a");//构造函数,传入一个Runnable对象,以及线程名
        Thread b = new Thread(r,"线程b");
        Thread c = new Thread(r,"线程c");
        a.start();
        b.start();
        c.start();

        System.out.println("main线程结束");
    }
}


main线程结束
Thread 线程a执行
Thread 线程b执行
Thread 线程c执行

线程启动较慢,主线程结束后其他线程仍然可以执行

Demo3

在demo2的基础上给主方法中添加 c.join();方法

public static void main(String[] args) throws InterruptedException {
        RunDemo r=new RunDemo();
        Thread a = new Thread(r,"线程a");//构造函数,传入一个Runnable对象,以及线程名
        Thread b = new Thread(r,"线程b");
        Thread c = new Thread(r,"线程c");
        a.start();
        b.start();
        c.start();

        c.join();
        System.out.println("main线程结束");
    }

//可能的结果之一
Thread 线程c执行
Thread 线程b执行
main线程结束
Thread 线程a执行

//无论什么结果,’Thread 线程c执行‘语句一定在‘main线程结束’语句之前

c线程使用了join方法,一定在主线程结束之前执行

Demo4

在以上的demo中继续改造:

//实现Runnable接口实现多线程
public class RunDemo implements Runnable{

    public void run() {//实现run方法

        System.out.println("Thread "+Thread.currentThread().getName()+"执行");

    }


    public static void main(String[] args) throws InterruptedException {
        RunDemo r=new RunDemo();
        Thread a = new Thread(r,"线程a");//构造函数,传入一个Runnable对象,以及线程名
        Thread b = new Thread(r,"线程b");
        Thread c = new Thread(r,"线程c");

        c.setPriority(10);//start()前就设置优先级
        a.start();
        b.start();
        c.start();

   
        System.out.println("a的优先级"+a.getPriority());
        System.out.println("b的优先级"+b.getPriority());
        System.out.println("c的优先级"+c.getPriority());
        System.out.println("main的优先级"+Thread.currentThread().getPriority());

        System.out.println("main线程结束");


    }
}


//结果1
a的优先级5
b的优先级5
c的优先级10
main的优先级5
main线程结束
Thread 线程c执行
Thread 线程a执行
Thread 线程b执行


//结果2
a的优先级5
Thread 线程c执行
b的优先级5
c的优先级10
Thread 线程a执行
main的优先级5
main线程结束
Thread 线程b执行

//cpu获得优先级后会按照优先级执行,但是前提是cpu必须获得线程信息
//虽然c的优先级设置为10,但是其他线程任由可能在c前执行

cpu必须获取到线程信息后才会按照优先级执行

Demo5

按照以上demo2继续改造:

Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。

public void run() {//实现run方法
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread "+Thread.currentThread().getName()+"执行");

    }


    public static void main(String[] args) throws InterruptedException {
        RunDemo r=new RunDemo();
        Thread a = new Thread(r,"线程a");//构造函数,传入一个Runnable对象,以及线程名
        Thread b = new Thread(r,"线程b");
        Thread c = new Thread(r,"线程c");

        c.setPriority(10);//start前就设置优先级
        a.start();
        b.start();
        c.start();


        Thread.yield();//让出资源
       

        System.out.println("a的优先级"+a.getPriority());
        System.out.println("b的优先级"+b.getPriority());
        System.out.println("c的优先级"+c.getPriority());
        System.out.println("main的优先级"+Thread.currentThread().getPriority());



        System.out.println("main线程结束");


    }
}

//run方法中Thread.sleep(100),cpu获得足够时间获取线程信息,此时线程c会在a、b前执行
a的优先级5
b的优先级5
c的优先级10
main的优先级5
main线程结束
Thread 线程c执行
Thread 线程a执行
Thread 线程b执行

//主线程中Thread.yield()方法没有使得main让出资源

Thread.yield()让出cpu,进入就绪状态。

yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

三、资源共享

使用多线程时一个重要的问题是共享资源,比如买票,大家都是在对同一个票仓进行操作,票售完即无法买票。

1、线程不安全

//实现Runnable接口实现多线程
public class RunDemo2 implements Runnable{

    private int count=20;

    public void run() {
        while(count>=0){
            System.out.println(Thread.currentThread().getName()+" "+"count: "+count);
            count--;

        }


    }


    public static void main(String[] args) {
        RunDemo2 r=new RunDemo2();

        Thread i1=new Thread(r,"线程1");
        Thread i2=new Thread(r,"线程2");
        Thread i3=new Thread(r,"线程3");


        i1.start();

        i2.start();

        i3.start();

    }
}

可能的结果:

线程1 count: 20
线程2 count: 20
线程3 count: 20
线程2 count: 18
线程1 count: 19
线程2 count: 16
线程3 count: 17
线程2 count: 14
线程1 count: 15
线程2 count: 12
线程3 count: 13
线程2 count: 10
线程1 count: 11
线程2 count: 8
线程3 count: 9
线程2 count: 6
线程1 count: 7
线程2 count: 4
线程3 count: 5
线程2 count: 2
线程1 count: 3
线程2 count: 0
线程3 count: 1

这显然不正常,多个对象操作同一数据,是把数据复制到自己的内存中进行操作,可能造成脏数据。

因此需要加锁,保证数据在某时刻只能被一个线程修改数据。

2、synchronized关键字

Jvm底部提供了synchronized关键字,synchronized是一种隐式锁。

  1. 每个对象都有一把锁
  2. synchronized可以用于方法上、静态方法上、以及代码块上

同步方法

作用在方法上,相当于拿到当前这个对象的锁,也就是this。

public synchronized void run() {

        System.out.println(Thread.currentThread().getName()+"进入run方法");
            while (count >= 0) {
                System.out.println(Thread.currentThread().getName() + " " + "count: " + count);
                count--;
            }

    }

可能的结果如下:

run方法被锁后,只能同时被一个线程进入该方法。其他线程只有等待这个锁释放后,才能进入方法中。

线程1进入run方法
线程1 count: 20
线程1 count: 19
线程1 count: 18
线程1 count: 17
线程1 count: 16
线程1 count: 15
线程1 count: 14
线程1 count: 13
线程1 count: 12
线程1 count: 11
线程1 count: 10
线程1 count: 9
线程1 count: 8
线程1 count: 7
线程1 count: 6
线程1 count: 5
线程1 count: 4
线程1 count: 3
线程1 count: 2
线程1 count: 1
线程1 count: 0
线程3进入run方法
线程2进入run方法

同步代码块

有时候我们不需要将整个方法都锁住,允许其他线程进入方法中读数据或者某些要求,只需要将部分代码锁住即可,那么可以使用代码块的方式。

用法如下:这里的obj就是要被锁的对象,每个对象都有锁,使用了不正确的锁可能还会造成错误数据。

所以建议要被修改的数据对象作为锁,保证该对象的数据正确。

 synchronized (obj){
    ......
 }

这里我直接使用当前对象作为锁:

public  void run() {

        System.out.println(Thread.currentThread().getName()+"进入run方法");
            synchronized (this){
                while (count >= 0) {
                    System.out.println(Thread.currentThread().getName() + " " + "count: " + count);
                    count--;
                }
            }

    }

以下是可能结果之一:允许多个线程进入方法、但是只允许一个线程修改数据。

线程1进入run方法
线程2进入run方法
线程1 count: 20
线程3进入run方法
线程1 count: 19
线程1 count: 18
线程1 count: 17
线程1 count: 16
线程1 count: 15
线程1 count: 14
线程1 count: 13
线程1 count: 12
线程1 count: 11
线程1 count: 10
线程1 count: 9
线程1 count: 8
线程1 count: 7
线程1 count: 6
线程1 count: 5
线程1 count: 4
线程1 count: 3
线程1 count: 2
线程1 count: 1
线程1 count: 0

同步静态方法

静态方法是类成员,在静态方法上使用synchronized关键字,此时的锁使用的对象是该类的Class对象,所以整个类都被锁住,该类的所有实例都加锁,只能有一个线程进入访问。

wait \notify

wait( ),notify( ),notifyAll( )都不属于Thread类,而是属于Object基础类,也就是每个对象都有wait( ),notify( ),notifyAll( ) 的功能,因为每个对象都有锁,锁是每个对象的基础,当然操作锁的方法也是最基础了。

用于处理线程之间的通信问题。

生产者、消费者

wait\notify解决的生产者、消费者问题。

//实现Runnable接口实现多线程
public class RunDemo2 implements Runnable{

    private  int count =0;

    public void run() {
        try {
            System.out.println(Thread.currentThread().getName()+"进入run()。。");
            System.out.println(Thread.currentThread().getName()+getCount());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
    
    public int getCount() throws InterruptedException {
        synchronized (this){
            System.out.println(Thread.currentThread().getName()+"进入get()。。。:count:"+count);
            this.wait();//当前线程等待
        }
        return count;
    }

    public void setCount(int count) {
        synchronized (this){
            System.out.println(Thread.currentThread().getName()+"进入set()。。。");
            this.notify();//唤醒线程
        }
        this.count = count;
    }

    public static void main(String[] args) throws InterruptedException {
        RunDemo2 r=new RunDemo2();

        Thread i1=new Thread(r,"线程1");
        Thread i2=new Thread(r,"线程2");
        Thread i3=new Thread(r,"线程3");


        i1.start();
        i2.start();
        i3.start();

    }
}



线程1进入run()。。
线程1进入get()。。。:count:0
线程2进入run()。。
线程2进入get()。。。:count:0
线程3进入run()。。
线程3进入get()。。。:count:0
    
//run方法没有加锁、所有线程都可以进入,但是get加锁了,进入之后就被wait方法等待了,此时没有调用对象notify唤醒线程,所以线程1.2.3都在等待,jvm仍然运行,控制台不会停止。

只要在调用set方法,this的锁就会被释放,所有进入run方法的线程

public static void main(String[] args) throws InterruptedException {
        RunDemo2 r=new RunDemo2();

        Thread i1=new Thread(r,"线程1");
        Thread i2=new Thread(r,"线程2");
        Thread i3=new Thread(r,"线程3");


        i1.start();
        i2.start();
        i3.start();


        Thread.sleep(1000);//让主线程延缓,其他线程能够完全启动

         r.setCount(100);//唤醒三次
         r.setCount(10);//有三个线程,所以唤醒三次
         r.setCount(1);

    }

//可能会是以下结果,仍旧是脏数据,原因如下:
线程1进入run()。。
线程1进入get()。。。:count:0
线程2进入run()。。
线程2进入get()。。。:count:0
线程3进入run()。。
线程3进入get()。。。:count:0
main  进入set()。。。
main  进入set()。。。
main  进入set()。。。
线程1 count: 1
线程2 count: 1
线程3 count: 1

//主线程连续三次调用set方法,在线程还没被完全唤醒就把数据完全覆盖了
 //尝试每修改前让主线程等待100ms
    
        Thread.sleep(1000);

        r.setCount(100);
        Thread.sleep(100);
        r.setCount(10);
        Thread.sleep(100);
        r.setCount(1);

//此时结果仍然是脏数据:原因如下:
线程1进入run()。。
线程3进入run()。。
线程2进入run()。。
线程3进入get()。。。:count:0
线程2进入get()。。。:count:0
线程1进入get()。。。:count:0
main  进入set()。。。
线程3 count: 0
main  进入set()。。。
线程2 count: 100
main  进入set()。。。
线程1 count: 10


//set方法中的返回值是在唤醒线程后才得以返回的,所以线程可能被唤醒后立即执行,读到的可能是脏数据
public void setCount(int count) {
        synchronized (this){
            System.out.println(Thread.currentThread().getName()+"  进入set()。。。");
            this.notify();//唤醒线程
        }
        this.count = count;
    }



//此时修改的方法,可以把run方法略微修改,让run方法中也sleep一段时间,这里休息的时间应该小于主线程中修改数据的间隔时间:Thread.sleep(100);,因为在run中sleep的时间如果长于主线程set的时间,那么数据以经被主线程多次修改了。
public void run() {
        try {
            System.out.println(Thread.currentThread().getName()+"进入run()。。");
            getCount();
            Thread.sleep(10);
            System.out.println(Thread.currentThread().getName()+" count: "+count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }



线程1进入run()。。
线程2进入run()。。
线程1进入get()。。。:count:0
线程2进入get()。。。:count:0
线程3进入run()。。
线程3进入get()。。。:count:0
main  进入set()。。。
线程1 count: 100
main  进入set()。。。
线程2 count: 10
main  进入set()。。。
线程3 count: 1

这种方法感觉还是比较麻烦的,jdk中提供的juc包相比之下更加好用。

3、volatile关键字

1、volatile关键字较为轻量,不加锁,用于修饰变量。

2、保证内存可见性、不保证原子性。

可见性

当对非volatile变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中,如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。
  volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。

原子性

单一操作,如i=1

i++就不是原子操作,先加一,再赋值给i.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!