栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

多线程-详解

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

多线程-详解

1 引言

随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。
那么话不多说,今天本帅将记录自己线程的学习。
程序,进程,线程的基本概念+并行与并发:
程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。-------生命周期

线程:进程可进一步细化为线程,是一个程序内部的一条执行路径
即:线程《线程(一个程序可以有多个线程)
程序:静态的代码
进程:动态执行的程序
线程:进程中要同时干几件事时,每一件事的执行路径成为线程。

并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事
并发:一个CPU(采用时间片)同时执行多个任务,比如秒杀平台,多个人做同件事

2 线程的相关API

//获取当前线程的名字
Thread.currentThread().getName()

start():1.启动当前线程 2 .调用线程中的run方法
run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
currentThread():静态方法,返回执行当前代码的线程
getName():获取当前线程的名字
setName():设置当前线程的名字
yield():主动释放当前线程的执行权
join():在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去
stop():过时方法。当执行此方法时,强制结束当前线程。
sleep(long millitime):线程休眠一段时间
isAlive():判断当前线程是否存活

3 判断是否是多线程

一条线程即为一条执行路径,即当能用一条路径画出来时即为一个线程
例:如下看似既执行了方法一,又执行了方法2,但是其实质就是主线程在执行方法2和方法1这一条路径,所以就是一个线程

public class Sample{
		public void method1(String str){
			System.out.println(str);
		}
	
	public void method2(String str){
		method1(str);
	}
	
	public static void main(String[] args){
		Sample s = new Sample();
		s.method2("hello");
	}
}

4 线程的调度

调度策略:
时间片:线程的调度采用时间片轮转的方式
抢占式:高优先级的线程抢占CPU
Java的调度方法:
对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
对高优先级,使用优先调度的抢占式策略

5 线程的优先级

等级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5

方法:
getPriority():返回线程优先级
setPriority(int newPriority):改变线程的优先级

注意!:高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行。

6 多线程的创建方式

方式1:继承于Thread类

创建一个集成于Thread类的子类 (通过ctrl+o(override)输入run查找run方法)
重写Thread类的run()方法
创建Thread子类的对象
通过此对象调用start()方法

7 start与run方法的区别:

start方法的作用:
1)启动当前线程
2)调用当前线程的重写的run方法(在主线程中生成子线程,有两条线程)
调用start方法以后,一条路径代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start方法。
run方法的作用:在主线程中调用以后,直接在主线程一条线程中执行了该线程中run的方法。(调用线程中的run方法,只调用run方法,并不新开线程)

总结:我们不能通过run方法来新开一个线程,只能调用线程中重写的run方法(可以在线程中不断的调用run方法,但是不能开启子线程,即不能同时干几件事),start是开启线程,再调用方法(即默认开启一次线程,调用一次run方法,可以同时执行几件事)

8 多线程例子(火车站多窗口卖票问题)

继承thread

package com.example.paoduantui.Thread;
	
	import android.view.Window;
	
	
	
	public class ThreadDemo extends Thread{
	
	    public static void main(String[] args){
	        window t1 = new window();
	        window t2 = new window();
	        window t3 = new window();
	
	        t1.setName("售票口1");
	        t2.setName("售票口2");
	        t3.setName("售票口3");
	
	        t1.start();
	        t2.start();
	        t3.start();
	    }
	
	}
	
	class window extends Thread{
	    private static int ticket = 100; //将其加载在类的静态区,所有线程共享该静态变量
	
	    @Override
	    public void run() {
	        while(true){
	            if(ticket>0){
	//                try {
	//                    sleep(100);
	//                } catch (InterruptedException e) {
	//                    e.printStackTrace();
	//                }
	                System.out.println(getName()+"当前售出第
                   "+ticket+"张票");
	                ticket--;
	            }else{
	                break;
	            }
	        }
	    }
	}

方式2:实现Runable接口方式

1)创建一个实现了Runable接口的类
2)实现类去实现Runnable中的抽象方法:run()
3)创建实现类的对象
4)将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
5)通过Thread类的对象调用start()

具体操作,将一个类实现Runable接口,(插上接口一端)。
另外一端,通过实现类的对象与线程对象通过此Runable接口插上接口实现

package com.example.paoduantui.Thread;
	
	public class ThreadDemo01 {
	    
	    public static  void main(String[] args){
	        window1 w = new window1();
	        
	        //虽然有三个线程,但是只有一个窗口类实现的Runnable方法,
              由于三个线程共用一个window对象,所以自动共用100张票
	        
	        Thread t1=new Thread(w);
	        Thread t2=new Thread(w);
	        Thread t3=new Thread(w);
	
	        t1.setName("窗口1");
	        t2.setName("窗口2");
	        t3.setName("窗口3");
	        
	        t1.start();
	        t2.start();
	        t3.start();
	    }
	}
	
	class window1 implements Runnable{
	    
	    private int ticket = 100;
	
	    @Override
	    public void run() {
	        while(true){
	            if(ticket>0){
	//                try {
	//                    sleep(100);
	//                } catch (InterruptedException e) {
	//                    e.printStackTrace();
	//                }
	                
                   System.out.println(Thread.currentThread().getName()+
                  "当前售出第"+ticket+"张票");
	                ticket--;
	            }else{
	                break;
	            }
	        }
	    }
	}

比较创建线程的两种方式:
开发中,优先选择实现Runable接口的方式
原因
1)实现的方式没有类的单继承性的局限性
2)实现的方式更适合用来处理多个线程有共享数据的情况
联系:Thread也是实现自Runable,两种方式都需要重写run()方法,将线程要执行的逻辑声明在run中

新增的两种创建多线程方式

实现callable接口方式:

与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果

package com.example.paoduantui.Thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class NumberThread implements Runnable{ @Override public void run() { for(int i = 0;i<=100;i++){ if (i % 2 ==0 ) System.out.println(Thread.currentThread().getName()+":"+i); } } } class NumberThread1 implements Runnable{ @Override public void run() { for(int i = 0;i<100; i++){ if(i%2==1){ System.out.println(Thread.currentThread().getName()+":"+i); } } } } public class ThreadPool { public static void main(String[] args){ //创建固定线程个数为十个的线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); //new一个Runnable接口的对象 NumberThread number = new NumberThread(); NumberThread1 number1 = new NumberThread1(); //执行线程,最多十个 executorService.execute(number1); executorService.execute(number);//适合适用于Runnable //executorService.submit();//适合使用于Callable //关闭线程池 executorService.shutdown(); } }

目前两种方式要想调用新线程,都需要用到Thread中的start方法。

9 java virtual machine(JVM):java虚拟机内存结构

程序(一段静态的代码)——————》加载到内存中——————》进程(加载到内存中的代码,动态的程序)
进程可细分为多个线程,一个线程代表一个程序内部的一条执行路径
每个线程有其独立的程序计数器(PC,指导着程序向下执行)与运行栈(本地变量等,本地方法等)

10 线程通信方法

wait()/ notify()/ notifayAll():此三个方法定义在Object类中的,因为这三个方法需要用到锁,而锁是任意对象都能充当的,所以这三个方法定义在Object类中。
由于wait,notify,以及notifyAll都涉及到与锁相关的操作
wait(在进入锁住的区域以后阻塞等待,释放锁让别的线程先进来操作)----
Obj.wait 进入Obj这个锁住的区域的线程把锁交出来原地等待通知:

notify(由于有很多锁住的区域,所以需要将区域用锁来标识,也涉及到锁) ----- Obj.notify 新线程进入Obj这个区域进行操作并唤醒wait的线程

所以wait,notify需要使用在有锁的地方,也就是需要用synchronize关键字来标识的区域,即使用在同步代码块或者同步方法中,且为了保证wait和notify的区域是同一个锁住的区域,需要用锁来标识,也就是锁要相同的对象来充当

11 线程的分类:

java中的线程分为两类:
1)守护线程(如垃圾回收线程,异常处理线程),
2)用户线程(如主线程)
若JVM中都是守护线程,当前JVM将退出。(形象理解,唇亡齿寒)

12 线程的生命周期:

JDK中用Thread.State类定义了线程的几种状态,如下:



线程的同步:在同步代码块中,只能存在一个线程。

13 线程的安全问题:

什么是线程安全问题呢?
线程安全问题是指,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。

上述例子中:创建三个窗口卖票,总票数为100张票
1)卖票过程中,出现了重票(票被反复的卖出,ticket未被减少时就打印出了)错票。
2)问题出现的原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也来操作车票。(将此过程的代码看作一个区域,当有线程进去时,装锁,不让别的线程进去)
生动理解的例子:有一个厕所,有人进去了,但是没有上锁,于是别人不知道你进去了,别人也进去了对厕所也使用造成错误。
3)如何解决:当一个线程在操作ticket时,其他线程不能参与进来,直到此线程的生命周期结束
4)在java中,我们通过同步机制,来解决线程的安全问题。

方式一:同步代码块

使用同步监视器(锁)
Synchronized(同步监视器){
//需要被同步的代码
}
说明:

操作共享数据的代码(所有线程共享的数据的操作的代码)(视作卫生间区域(所有人共享的厕所)),即为需要共享的代码(同步代码块,在同步代码块中,相当于是一个单线程,效率低)

共享数据:多个线程共同操作的数据,比如公共厕所就类比共享数据
同步监视器(俗称:锁):任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁住以后只能有一个线程能进去(要求:多个线程必须要共用同一把锁,比如火车上的厕所,同一个标志表示有人)
Runable天生共享锁,而Thread中需要用static对象或者this关键字或者当前类(window。class)来充当唯一锁

方式二:同步方法

使用同步方法,对方法进行synchronized关键字修饰
将同步代码块提取出来成为一个方法,用synchronized关键字修饰此方法。
对于runnable接口实现多线程,只需要将同步方法用synchronized修饰
而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)

总结:
1)同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
2)非静态的同步方法,同步监视器是this
静态的同步方法,同步监视器是当前类本身。继承自Thread。class

package com.example.paoduantui.Thread;

import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{
    private int ticket = 100;//定义一百张票
    //1.实例化锁
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
            while (true) {
                //2.调用锁定方法lock
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
                    ticket--;
                } else {
                    break;
                }
            }
        }
}

public class LockTest {

    public static void main(String[] args){
       Window w= new Window();

       Thread t1 = new Thread(w);
       Thread t2 = new Thread(w);
       Thread t3 = new Thread(w);

       t1.setName("窗口1");
       t2.setName("窗口1");
       t3.setName("窗口1");

       t1.start();
       t2.start();
       t3.start();
    }
}
14 总结:Synchronized与lock的异同?

相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的代码逻辑以后,自动的释放同步监视器
lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(同时以为着lock的方式更为灵活)

优先使用顺序:
LOCK-》同步代码块-》同步方法

判断线程是否有安全问题,以及如何解决:

1)先判断是否多线程
2)再判断是否有共享数据
3)是否并发的对共享数据进行操作
4)选择上述三种方法解决线程安全问题

例题:

package com.example.paoduantui.Thread;
	
	
	
	//账户
	class Account{
	    private double balance;//余额
	    //构造器
	    public Account(double balance) {
	        this.balance = balance;
	    }
	    //存钱方法
	    public synchronized void deposit(double amt){
	        if(amt>0){
	            balance +=amt;
	            try {
	                Thread.sleep(1000);
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	            System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance);
	        }
	    }
	}
	
	//两个顾客线程
	class Customer extends Thread{
	     private Account acct;
	
	     public Customer(Account acct){
	         this.acct = acct;
	     }

	    @Override
	    public void run() {
	        for (int i = 0;i<3;i++){
	            acct.deposit(1000);
	        }
	    }
	}
	
	//主方法,之中new同一个账户,甲乙两个存钱线程。
	public class AccountTest {
	
	    public static void main(String[] args){
	        Account acct = new Account(0);
	        Customer c1 = new Customer(acct);
	        Customer c2 = new Customer(acct);
	
	        c1.setName("甲");
	        c2.setName("乙");
	
	        c1.start();
	        c2.start();
	    }
	
	}
15解决单例模式的懒汉式的线程安全问题:

单例:只能通过静态方法获取一个实例,不能通过构造器来构造实例
1.构造器的私有化:

private Bank(){}//可以在构造器中初始化东西
private static Bank instance = null;//初始化静态实例

public static Bank getInstance(){
    if(instance!=null){
        instance = new Bank();
    }
    return instance;
}

假设有多个线程调用此单例,而调用的获取单例的函数作为操作共享单例的代码块并没有解决线程的安全问题,会导致多个线程都判断实例是否为空,此时就会导致多个实例的产生,也就是单例模式的线程安全问题。

解决线程安全问题的思路:

将获取单例的方法改写成同部方法,即加上synchronized关键字,此时同步监视器为当前类本身。(当有多个线程并发的获取实例时,同时只能有一个线程获取实例),解决了单例模式的线程安全问题。
用同步监视器包裹住同步代码块的方式。

懒汉式单例模式的模型,例如:生活中的限量版的抢购:
当一群人并发的抢一个限量版的东西的时候,可能同时抢到了几个人,他们同时进入了房间(同步代码块内)
但是只有第一个拿到限量版东西的人才能到手,其余人都不能拿到,所以效率稍高的做法是,当东西被拿走时,我们在门外立一块牌子,售罄。
这样就减少了线程的等待。即下面效率稍高的懒汉式写法:

package com.example.paoduantui.Thread;

public class Bank {
    //私有化构造器
    private Bank(){}
    //初始化静态实例化对象
    private static  Bank instance = null;

    //获取单例实例,此种懒汉式单例模式存在线程不安全问题(从并发考虑)

    public static  Bank getInstance(){
        if(instance==null){
            instance = new Bank();
        }
        return  instance;
    }

    //同步方法模式的线程安全
    public static synchronized Bank getInstance1(){
        if(instance==null){
            instance = new Bank();
        }
        return  instance;
    }
    
    //同步代码块模式的线程安全(上锁)
    public  static Bank getInstance2(){
        synchronized (Bank.class){
            if(instance==null){
                instance = new Bank();
            }
            return  instance;
        }
    }
    
    //效率更高的线程安全的懒汉式单例模式
    
    public static Bank getInstance3(){
        if (instance==null){
            synchronized (Bank.class){
                if(instance==null){
                    instance = new Bank();
                }
            }
        }
        return  instance;
    }
}
16 线程的死锁问题:

线程死锁的理解:僵持,谁都不放手,一双筷子,我一只你一只,都等对方放手(死锁,两者都进入阻塞,谁都吃不了饭,进行不了下面吃饭的操作)
出现死锁以后,不会出现提示,只是所有线程都处于阻塞状态,无法继续

package com.example.paoduantui.Thread;



public class Demo {

    public static void main(String[] args){

        final StringBuffer s1 = new StringBuffer();
        final StringBuffer s2 = new StringBuffer();
        
        new Thread(){
            @Override
            public void run() {
                //先拿锁一,再拿锁二
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");

                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        //使用匿名内部类实现runnable接口的方式实现线程的创建
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");

                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }

}

死锁的解决办法:
减少同步共享变量
采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
减少锁的嵌套。

17 线程的通信


使用前提:这三个方法均只能使用在同步代码块或者同步方法中。

package com.example.paoduantui.Thread;



class Number implements Runnable{

    private int number = 1;//设置共享数据(线程之间对于共享数据的共享即为通信)

    //对共享数据进行操作的代码块,需要线程安全
    @Override
    public synchronized void run() {
        while(true){
            //使得线程交替等待以及通知交替解等待
            notify();//省略了this.notify()关键字
            if(number<100){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+number);
                number++;
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                break;
            }
        }
    }
}

public class CommunicationTest {

    public static void main(String[] args){
        //创建runnable对象
        Number number = new Number();

        //创建线程,并实现runnable接口
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        //给线程设置名字
        t1.setName("线程1");
        t2.setName("线程2");

        //开启线程
        t1.start();
        t2.start();
    }
}
18 sleep和wait的异同:

相同点:一旦执行方法以后,都会使得当前的进程进入阻塞状态
不同点:
1)两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。
2)调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
3)关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放
经典例题:生产者/消费者问题:

生产者(Priductor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如20个),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产:如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

这里可能出现两个问题:
生产者比消费者快的时候,消费者会漏掉一些数据没有收到。
消费者比生产者快时,消费者会去相同的数据。

package com.example.paoduantui.Thread;




class Clerk{
	
	    private int productCount = 0;
	    //生产产品
	    public synchronized void produceProduct() {
	        if(productCount<20) {
	            productCount++;
	            System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产
	            品");
	            notify();
	        }else{
	            //当有20个时,等待wait
	            try {
	                wait();
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	    }
	
	    //消费产品
	    public synchronized void consumeProduct() {
	        if (productCount>0){
	            System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个产
	            品");
	            productCount--;
	            notify();
	        }else{
	            //当0个时等待
	            try {
	                wait();
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	    }
	}
	
	class Producer extends Thread{//生产者线程
	
	    private Clerk clerk;
	
	    public Producer(Clerk clerk) {
	        this.clerk = clerk;
	    }
	
	    @Override
	    public void run() {
	        try {
	            sleep(10);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        System.out.println(Thread.currentThread().getName()+";开始生产产品......");
	
	        while(true){
	            clerk.produceProduct();
	        }
	    }
	}
	
	class Consumer implements Runnable{//消费者线程
	
	    private Clerk clerk;
	
	    public Consumer(Clerk clerk) {
	        this.clerk = clerk;
	    }
	
	    @Override
	    public void run() {
	        System.out.println(Thread.currentThread().getName()+":开始消费产品");
	        while(true){
	            try {
	                Thread.sleep(1);
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	            clerk.consumeProduct();
	        }
	
	    }
	}
	
	public class ProductTest {
	
	    public static void main(String[] args){
	        Clerk clerk = new Clerk();
	
	        Producer p1 = new Producer(clerk);
	        p1.setName("生产者1");
	
	        Consumer c1 = new Consumer(clerk);
	        Thread t1 = new Thread(c1);
	        t1.setName("消费者1");
	
	        p1.start();
	        t1.start();
	    }
	}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/319189.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号