最全java多线程

1.多线程操作是编程中的基本操作,多线程可以实现多个任务并发的执行(注意是并发的执行并不是并行)。java对多线程编程有着很好的支持。我来看一下线程相关的操作以及线程中常见的设计模式

2.两种方法实现多线程:

继承Thread类:继承Thread类实现线程需要复写run方法

    我们在这里定义实现两个线程,继承Thread类。分别是Ra,Ta,再在test方法里面启动线程。注意,启动线程必须调用start方法。而不是调用run方法。调用start方法使得该线程进入就绪状态,获取CPU即可执行。下边是代码

package thread;

public class Ra extends Thread{
    //复写run方法
   public void run()
   {
       for(int i=0;i<100;i++)
       {
           System.out.println("Ra-->"+i);
       }
   }
 
}

package thread;

public class Ta extends Thread{
 
    public void run()
    {
        for(int i=0;i<100;i++)
        {
        System.out.println("TO-->"+i);
        }
    }
}

package thread;

public class test {

     public static void main(String[]args)
       {
           //创建子类对象,调用start方法。不用直接调用run方法,这个run会由CPU自动启动
           Ra a=new Ra();
           Ta t=new Ta();
           a.start();
           t.start();
           /*
            * 线程数量的分析:这里面一共由四个线程,首先是主线程main,ra,ta两个手动的定义的线程
            * 最后是后台的垃圾回收GC线程
            */
           for(int i=0;i<100;i++)
           {
               System.out.println("main-->"+i);
           }
       }

}

/*
 * 使用继承实现多线程有一个缺点:java只能是单继承,这就限制了这个类只能继承Thread,而不能继承其他的类
*虽然java不能够多继承,但是可以实现多个接口,在多线程中也提供了这样一种实现多线程的方法,就是实现Runnbale接口
*/

3.在采用Runnable实现多线程的之前我们首先看一个设计模式:静态代理设计模式,也许你会想为什么是静态代理模式而不是动态代理,没错,的却有动态代理,那是在反射里面讲的。今天我们先看一看静态代理设计模式。

4.所谓代理,就是有代替的意思,在静态代理中我们有两个类实现一个共同的接口。在一个代理类中调用真实的类对应的方法,而在代理类中我们可以代替这个真实的类做一些事情。下边是对应的代码
/**
 * 之所以学习这个静态代理模式,因为多线程的第二种方法的实现就是这种方法
 * 其中Thread就是代理方,我们只需要实现自己真实的线程就可以
 *
 */

interface M//共同的接口M
{
    void set();//代理类和真实类都需要实现的方法
}
class you implements M
{
    public void set()
    {
        System.out.println("this is true ");
    }
}
class proxy implements M
{
    //需要传入真实用户的引用
    private you y;
    proxy(){};
    proxy(you y)
    {
        this.y=y;
    }
    public void before()
    {
        System.out.println("这是代理之前做的事情");
    }
    public void after()
    {
        System.out.println("这是代理之后的事情");
    }
    public void set()
    {
        this.before();
        this.y.set();//调用真实的应用
        this.after();
    }
}

public class StaticProxy {

    public static void main(String[] args)
    {
        you m=new you();
       proxy p=new proxy(m);
       p.set();
    }

}

5.在实现Runnable接口来实现多线程的过程中,就是静态代理模式的一个应用。其中代理方就是Thread类,它实现了Run函数,我们实现Runable接口中的run函数即可,将我们真实的操作写在这个run函数中。再通过Thread代理实现。我们来简单的实现一下,和上一个差不多,我们继续采用两个类:RunRa,RunTa作为两个线程,TestRun来测试这两个线程.。下面是具体的代码实现

package thread;

public class RunRa implements Runnable{

    @Override
    public void run() {
        for(int i=0;i<100;i++)
        {
            System.out.println("RunRa-->"+i);
        }
    }
   
}

package thread;

public class RunTa implements Runnable {
   public void run()
   {
       for(int i=0;i<100;i++)
       {
           System.out.println("RunTa-->"+i);
       }
   }
}

package thread;

public class TestRun {

    public static void main(String[] args) {
        //通过实现Runnable接口实现多线程
        Thread t1=new Thread(new RunRa());
        Thread t2=new Thread(new RunTa());
        t1.start();
        t2.start();

    }

}

6.以上就是实现多线程的常用的两种方式,但是我们可以发现一个问题,就是run方法返回任何值,而且线程不对外声明异常,所以就算是出现错误我们很难检测到。基于这一想法,我们还有一种实现多线程的方式,就是实现Callable接口,首先里里里面的call方法。这里我们暂时不做深究,因为这是java并发编程里面的内容,我们暂时只是实现一下这个多线程,具体代码如下:

package thread;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestCallable {
    public static void main(String[]args) throws Exception, ExecutionException
    {
        //创建线程.这是一个线程池
        ExecutorService ex=Executors.newFixedThreadPool(1) ;
        race r=new race();//创建实例对象
        //获取值
        Future result=ex.submit(r);
        int num=(int)result.get();
        System.out.println(num);
        //这个线程执行以后主线程并没有结束,我们还需要停止服务
        ex.shutdown();
    }

}
class race implements Callable<Integer>
{

    @Override
    public Integer call() throws Exception
    {
        return 1000;
    }
    
}

7.总结一下:实现多线程的三种方法,继承Thread类,实现Runnabl接口,实现Callable接口。其中最常用的还是实现Runnable接口,因为一般继承的位置需要预留出来继承其他的类,Callable的实现过于复杂。学习完这些以后我们来考虑一个多线程中经典的应用--购票问题,也引出了多线程中最棘手的问题--线程的同步。

8.生活中我们出行需要购买车票,你可以在各个车站买到同一张车票,比如说你需要一张从武汉到北京的高铁票。你既可以在武汉站买也可以在北京站买,甚至全国其他发售全国的车票的站点都可以买。但是我们知道车票是唯一的,同一辆车上不存在两张一模一样的车票,也就是说这些车站共享一份车票,一张车票卖出以后,数据库中就应该注销这张车票使得其他各个站点都无法再卖。这里面的站点,就是我们上边讨论的代理,而我们自己就是真实的用户,他们代理我们购买车票并且把车票在数据库中注销掉。每一个站点都是一个线程,共享一份资源,同一时刻应该只有一个站点占用这一份资源,也就是说同一时刻的一张票只能由一个站点卖出去,其他站点在这段时间都无法访问这张票。但是我们刚刚上边实现的多线程并不是这样的,虽然也是可以同一个线程共享一份资源,但是同一时刻对于一份资源却是可以多个线程都可以访问,如何解决这个问题?这就是线程的同步问题,为了解决这个问题,便出现了线程锁这个工具。其实这个思想很简单:线程锁想要做的事情就是,将资源锁起来,钥匙有且只有一把,所有的线程来争夺这把钥匙,谁抢到了就可以执行线程。其他的线程等待,执行完以后放弃这把锁,所有线程继续来争夺。那么问题又来了,锁哪里?怎么锁?这些都是问题。

9.synchronized关键字。

(1)锁定区域:synchronized(object){  // 这里是锁定的区域}。其中object是一个对象,指定我们需要锁定的对象,一般都是this。当然,对于静态的类和函数没有this关键字,我们后边再讨如何来锁定这种类。

(2) public  synchronized void test() throws Exception
    {           //函数体。                    }这个将synchronized关键字直接加在函数返回值的前边,锁定整个方法。这里我们稍微的深入一点,很多人对线程锁的理解就是线程锁锁住的就是代码,之所以其他线程不能执行是因为代码被锁住,其他线程不能够访问这些代码,但是这是1一种错误的观点,线程锁锁住的绝对不是代码,二是这个函数执行的权力,其实所有线程都可以进入函数但是无法执行,因为没有获得这个权利。具体想深入了解的,可以看一下线程锁的底层实现。

10.解决了锁的问题,我们来实现一下这个购票的程序,我们采用两个类,一个是sale类,用来买票,然后开启三个线程作为三个代理点。代码如下:

package lock;
/**
 * (1)同步方法:
 * 这是一个简单的抢票程序:首先需要注意的是线程之间的同步,采用线程同步送锁定当前线程
 * 只有获取到同步锁的就绪线程才可开始执行这保证了数据的同步,多个线程共享一份数据
 *线程的是实现采用静态代理的模式,代理为Thread类,这个类中已经实现了run()函数
 *我们只需要实现自己的run函数,也就是真实的run。这种实现多线程的方法叫做实现Runnable接口实现
 *注意同步方法锁住的不是代码,锁住的是执行的权力。其他线程依旧可以获取该方法但是不可以执行
 *(2)同步块:只是锁住某一个区域
 */
public class sale  implements Runnable {
    private int num=50;//票的总数
    private boolean flag=true;
    public void run()
    {
        while(true)
        {
            
        try
        {
            test();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println(e);
        }
        }
    }
    public  synchronized void test() throws Exception
    {
        synchronized(this)
        {
        if(num<=0)
        {
            flag=false;
            return ;
        }
        else
        {
            System.out.println(Thread.currentThread().getName()+"抢到第"+"-->"+num--+"票");
            Thread.sleep(1000);
        }
        }
        
    }
}
 

package lock;
import java.util.*;
public class test {

    public static void main(String[] args)
    {
        sale s=new sale();
        Thread t1=new Thread(s,"路人甲");
        Thread t2=new Thread(s,"路人乙");
        Thread t3=new Thread(s,"路人丙");
        Thread t4=new Thread(s,"路人丁");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

}


11.其实人们早就知道,工具是一把双刃剑,没错,线程锁也是这样,线程锁的出现很好的解决了线程之间的同步。但是,这也出现了很多新的问题,首先因为线程锁的存在,一个线程获得线程锁开始执行那么其他线程必须等待,这就拖慢了程序的运行速度,降低了性能。这也就是为什么我们说线程安全的方法通常性能低下,因为线程等待的存在。同时,我们对于线程锁的应用也很难掌握,因为有的被锁住的区域大了会降低性能,小了就会失效。所以线程锁的使用十分灵活。在这些问题有一个很容易出错的地方:线程的死锁。线程的死锁其实很简单:鸡生蛋蛋生鸡这种鸡蛋问题,不对,不形象,更加准确的描述就是有一个场景:警匪问题,歹徒劫持了人质,要求警察放他走才释放人质,否则(此处省略n字,因为毕竟这个性别有关对吧,哈哈,不开车了。)警察则说:你放了人质我就放你走(当然我们知道这是不可能的),歹徒知道这是假的,就不放人质,警察因为歹徒不放人质就不肯放他走,然后就僵持下来。线程的死锁就是这种关系,两个线程各自锁住了另一个线程中的一些执行时候必要的资源,结果导致了两个线程都不能执行。当然,真实的情况不仅仅是两个线程之间的死锁,而是多个线程之间的互相死锁。所以具体代码这个我就不实现了,自己可以实现一下,难度不大。

12.在9中我们讨论synchronized的用法,我们只是锁了有this关键字的区域,当然对于一些静态的方法根本没有this关键字,这个时候怎么实现线程的同步呢?方法总比问题多,这个问题设计Java的大牛早就想到了,同时也给出了解决方案。在了解这个东西之前我们有必要了解一下一种设计模式:单例设计模式。单例设计模式顾名思义就是只有一个例子,指的是这个类只有一个对象,并且这个对象是在类的内部,类外不能定义对象。注意理解这个不能的定义:不能定义的含义是你不能new对象(看到这里我想起了c++中程序员为了避免在堆上产生对象采取的一种手段:构造函数私有化。什么?为什么避免在对上产生对象?因为c++

没有GC,所有的堆内存需要你自己管理,这里也是c++程序崩溃的一个主要原因。是不是突然觉得Java大法好!)这里也是这个思想,单例的实现有多种方式,既然我们叫最全线程教程,那么我们就全部来写一下。

(1)懒汉式的单例:

        1.构造器私有化;

        2.定义静态私有的成员对象;

        3.定义公共的外部访问1内部静态数据的方法。

package test;

public class Jvm {
private static Jvm ins=null;//静态私有的类成员,之所以叫做懒汉,是因为这里首先赋值为空,用的时候再构造
private Jvm() {};//构造器私有化
public static Jvm getIns()
{

  if(ins==null)
    {
        ins=new Jvm();//用的时候再初始化
    }
    return ins;
}
}

(2)饿汉式单例:饿汉式单例之所以叫做饿汉式,区别就是直接初始化内部数据。代码如下:

class Myjvm{
    private     static Myjvm jvm=new Myjvm();//类加载的时候就赋值,饿汉式
    private Myjvm()
    {
        //私有化构造器
    }
    public static Myjvm getjvm()
    {
        return jvm;
    }
}

我们分下这两种实现的方式:首先是饿汉式,如果现在有大量的线程进来,其实我们只需要一个线程进去,然后初始化内部对象在后的各个线程直接获取就行了,所以我们可以加一个判断。然后事情并非你想的那么简单(没错,刚才是我自己想的),你让一个进去就一定是一个进去吗,万一两个线程同时进去出来之后就不一定是一个对象了,所以我们还需要对这个初始化对象的方法加锁,那么问题又来了,同时进来的其他线程在当前获取线程锁的线程执行完之后也没必要继续执行了,直接获取执行的结果就行了,所以我们还可以加一层判断。这样我们就有了两层判断加一个锁。这就是很著名的double-checking。对于饿汉式,我们知道,一个类的静态成员初始化的时机在被类加载器加载之后就初始化了,而类加载是这个类在使用的时候才会别加载(这是jvm的实现,我接下来会更新一点深入一点jvm的博文,重点在类加载机制,tomcat的类加载,以及GC的底层实现和源码分析,敬请期待)但是我们只需要在用的时候再初始化,所以这明显不符合我们的要求(其实是降低了程序的性能)。所以我们要求使用的时候在来初始化。其实这两个分析都是从提高程序的性能的角度出发。下边我们来升级一下

(3)升级版的懒汉式——double-checking.

class Jvm
{
    private static Jvm ins=null;//懒汉式的做法,首先不构建对象,等到用的时候再构建
    private Jvm()
    {
        
    }
    public static  Jvm Getins(long time)
    {
        //提高效率,我们直到对对象只是创建一次,所以只需要加一层判断如果该对象创建成功,那么我们只需要
        //返回就好,不需要继续创建
        if(ins==null)
        {

            /*
             * 由于这里会出现线程的等待,所以在多线程里面这里是不安全的,我们需要在这里加锁
             * 代码分析:首先内部的if判断ins是否为空。如果此时有五个线程,ab同时进入程序,a
             * 获取到线程锁执行,那么后来的cde就不用进入直接获取a创建成功的对象,b当a释放线程
             * 锁之后直接获取(注意这里的区别:b是进入内部之后在获取)。
             */
        synchronized(Jvm.class)
        {
        try
        {
            Thread.sleep(time);
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        if(ins==null)
        {
            ins=new Jvm();
            
        }
        return ins;
            
        }
        }
        return ins;
    }
}

(4)升级版的饿汉式-采用内部类实现'动态的加载':

class Myjvm2
{
    private static  class inner
    {
        private     static Myjvm2 jvm=new Myjvm2();
    }
    //私有化构造器
    private Myjvm2()
    {
        
    }
    
    public Myjvm2 getjvm()
    {
        return inner.jvm;
    }
}

再回过来看我们的问题:如何锁住静态的方法,在double-checking模式中我们已经给出了,依然是采用synchronized关键字,但是参数换为了该单例的字节码文件。这就是解决方法。

13.以上就是我们对于线程的学习,说是最全,这是开玩笑的,你知道线程为什么会出现吗?线程之间是并发执行的,但是你了解什么并发吗?一起执行?显然不是,那叫做并行,又出来一个概念--并行。所以我们这寥寥数语(其实也不少了),想搞清楚并熟练应用,是一个很大的挑战。我们这里只是皮毛,但是是通往深处的必经之道。有个人曾将对我说他学java的感受:会用可能只是几个月,但是搞明白他用了几年。

14.线程中的一些其他的小知识

  (1)sleep与wait的区别。这两个方法都可以是线程停下来,但是有本质的区别,sleep只是让线程停止一段时间,就像是睡着了,会自己醒来,并且在睡眠的时间内不会放弃线程锁(假设他已经获取了线程锁),wait会让当前线程放弃线程锁,并且需要唤醒。

(2)线程的优先级,如果想要某个线程执行的概率尽可能的大,可以使用setPriority(newPriority);来设置线程的优先级,范围是0-10,但是不是说线程优先级越大就一定会执行只是说概率大。具体实现的原理我们可以去看一下源码。

(3)Provide-Consumer。生产者-消费者模式,注意这不是一个设计模式,这只是一种多线程的架构。设计模式指的是类与类之间的关系,实现这种模式可以采用经典的信号灯法,这里就不继续了。

 (4)线程的调度,据问题而定,没有什么放之四海皆准的法则。

   

猜你喜欢

转载自blog.csdn.net/weixin_41863129/article/details/81839632
今日推荐