浅谈Java多线程

Java多线程的概念及创建方法

一、首先我们需要明白几个概念:程序、进程和线程

程序:指令集,是一个静态概念,比如说桌面上的一个应用就是一个程序。不管它运不运行都是一个程序

进程:操作系统调度程序,是一个动态概念。还是拿上面那个例子,当我们点击运行的时候,操作系统开始调度,这就启动了一个进程。

线程:线程是程序运行 的最小单位。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。(百度上copy过来的,哈哈),它和进程的关系是在一个进程内可以有多个线程,实际上它就是程序运行之后,进程执行的多条路径。多线程就是指一个进程可以有多条执行路径。比如说:一个人在桌子上吃饭,这是单进程单线程。多个人在一桌吃饭,这是单进程多线程。简而言之就是,有多个人同时使用同一个资源,这就是多线程。多线程涉及到的主要有并发、死锁、线程安全等问题。接下来就来具体说一下多线程是怎么实现的

二、线程实现的几种方法

在Java里面实现线程的方法主要由三种:

1、继承Thread类+实现run()方法

贴上代码:

/**
 * 创建多线程,继承Thread,重写run方法
 */
public class ThreadPra extends Thread {
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println("跑了"+i +"步");
        }
    }

    public static void main(String[] args) {
//        新生线程
        Thread t1=new Thread();
        Thread t2= new Thread();
//        启动线程
        t1.start();
        t2.start();
    }
}

2、实现Runnable接口+run方法

public class Ticket implements Runnable {
    private int num= 50;
    @Override
    public void run() {
        while (true){
            if (num<=0){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
        }
    }

    public static void main(String[] args) {
        Ticket t = new Ticket();
        Thread t1 = new Thread(t,"路人甲");
        Thread t2= new Thread(t,"黄牛");
        Thread t3 = new Thread(t,"程序员");//线程新生
        t1.start();//线程就绪
        t2.start();
        t3.start();
    }
}

运行结果:


可以看到这个抢占资源的顺序是随机的。谁抢到了资源谁就先运行。

由于继承Java的单继承,因此推荐使用Runnable接口,优点1避免单继承和2便于资源共享

启动:使用静态代理

1)创建真实角色
2)创建代理角色Thread+引用

3)代理角色.start()

缺点:不能抛出异常,没有返回值!!

这样的实现是不是看起来很简单!!!

3、实现Callable接口,重写call方法,用Future获取返回值

使用这种方式。我们需要借助任务调度线程池ScheduledThreadPoolExecutor,比如我们如果需要运行两个线程,我们可以用:

ExecutorService exe = new ScheduledThreadPoolExecutor(2);

指定线程的个数为2,

 然后创建新的线程:(这里的Race实现了Callable接口,重写了call方法)

Race l =new Race("tortoise",1000);
Race r = new Race("rabbit",500);
通过result.get()获取到返回值
//        获取返回值
        Future<Integer> res1 = exe.submit(l);
        Future<Integer> res2 = exe.submit(r);

获取返回值,最后停止服务

exe.shutdownNow();

完整的代码段:

ExecutorService exe = new ScheduledThreadPoolExecutor(2);
        Race l =new Race("tortoise",1000);
        Race r = new Race("rabbit",500);
//        获取返回值
        Future<Integer> res1 = exe.submit(l);
        Future<Integer> res2 = exe.submit(r);

        Thread.sleep(4000);//4秒就停
        l.setFlag(false);
        r.setFlag(false);
        System.out.println("乌龟跑了"+ res1.get()+"");
        System.out.println("兔子跑了"+ res2.get()+"");
//        停止服务
        exe.shutdownNow();

这种方式的优点是:1)可以对外声明异常2)有返回值。缺点是比较繁琐

三、线程的状态

简单来说线程一个有五个主要的状态:新生、就绪、运行、阻塞、死亡

那么这几个状态之间怎么转换?以下图来说明,注意:阻塞解除后会进入就绪状态


1、新生状态(new )
2、就绪状态(线程准备就绪,待CPU分配时间片即可运行)
3、CPU给了时间片,线程进入运行状态
4、阻塞状态 如sleep或者等待I/O设备等资源

5、死亡状态(两种情况:一种是正常死亡,线程体正常执行完毕。一种是外部干涉,调用stop或者destroy强行终止,不会释放锁)

涉及到同步的具体的几种状态:


这里的重点是怎么样创建线程终止线程。创建线程前面已经说过了。这里我们看一下终止线程的几种方法。

第一种是线程运行完毕,自然终止(死亡)。

第二种是外部干涉。外部干涉也有几种方式。

1)线程类中定义线程体使用的标识如flag标识 2)线程体使用该标识 3)提供对外的方法改变该标识 4)外部根据条件调用该方法即可

线程阻塞的方法:1)join合并线程 2)yield暂停当前正在执行的线程对象,它是一个静态方法,并执行其他线程 3)sleep休眠,这个是用的比较多的一个方法。调用这个方法后,线程进入休眠状态,不会释放锁,此时若有新的线程则会进行等待。

sleep方法示例:

public class sleepDemo {
    public static void main(String[] args) throws InterruptedException {
        Date endTime = new Date(System.currentTimeMillis()+10*1000);
        long end =endTime.getTime();
        while (true){
//            输出
            System.out.println(new SimpleDateFormat("hh:mm:ss").format(endTime));
//            休眠
            Thread.sleep(1000);
//            构建下一秒的时间
            endTime = new Date(endTime.getTime()+1000);

            if(endTime.getTime() -10000> end){
                break;
            }
        }
    }
}

其他的线程的信息:

1、Thread.currentThread获得当前线程。2、获取名称、设置名称、优先级、判断状态

proxy.setPriority(Thread.MIN_PRIORITY);
proxy.isAlive()

四、线程同步(并发)

同步(并发):多个线程访问同一个资源时,我们要确保这份资源的安全性。比如说银行取钱的时候,多个人操作相同的账户,存取钱。如果处理不好就可能会发生不安全的问题。为了避免这种问题,我们需要采取相应的措施。

解决并发问题所采取的主要由几种方法:

1、给线程加锁。java里面采取使用synchronized关键字的方式。synchronized关键字 ->同步

Java里面同步有同步块和同步方法

同步块:

synchronize(引用类型|this类.class){
//方法体
}

同步方法:

在方法中加入synchronized关键字,保证线程安全。

难点:线程范围不能太大,否则浪费资源,造成等待,也不能过小,否则造成线程不安全

五、死锁

死锁是由什么引起的呢?

过多的并发,造成资源等待,比如我们操作系统书里面提到的筷子问题,每个人都在等待旁边的人释放筷子。这就造成了无限等待,也就造成了死锁问题。这个时候我们就需要采取相应的措施。

解决死锁问题的两个个典型的方法是生产者---消费者模式、管程法

生产者消费者模式:也称有限缓冲问题,让生产者在缓冲区满时休眠,等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒。

代码:

场景:比如我们定义一个场景Movie,Player和Watch都共同使用这个资源,这就涉及到了同步问题。

生产者使用资源Movie:

/**
 * 生产者
 */
public class Player implements Runnable{
    private Movie m;
    public Player(Movie m){
        super();
        this.m=m;
    }
    @Override
    public void run() {
        for(int i= 0;i<10 ;i++){
            if(0 == i%2){
                m.play("");
            }else{
                m.play("");
            }
        }
    }
}

消费者(也使用Movie):

/**
 * 消费者
 */
public class Watch implements Runnable {
    private Movie m;
    public Watch(Movie m){
        super();
        this.m=m;
    }
    @Override
    public void run() {
        for(int i= 0;i<10 ;i++){
            m.watch();
        }

    }
}

定义两个类共用movie资源,我们定义flag信号灯。

/**
 * 一个场景,共同的资源
 * 生产者消费者模式,信号灯法
 * wait()
 */
public class Movie {
    private String pic;

//    信号灯
//    flag -->T生产者生产,消费者等待,生产完成后通知消费者
//    flag -->F 消费者消费,生产者等待,消费完成后通知生产者

    private boolean flag = true;
    public synchronized void play(String pic){
        if (!flag){//生产者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        开始生成
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//        生产完毕
        System.out.println("生产了:"+pic);
        this.pic = pic;
//        通知消费
        this.notify();
//        生产者停下
        this.flag = false;
    }
    public synchronized void watch(){
        if(flag){//消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        开始消费
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//        消费完毕
        System.out.println("消费了了:"+pic);
//        通知生产
        this.notifyAll();
//        消费者停下
        this.flag = true;

    }
}

main方法:

public class App {
    public static void main(String[] args) {
        Movie m =new Movie();

//        多线程
        Player p = new Player(m);
        Watch w = new Watch(m);
//        访问同一份资源
        new Thread(p).start();
        new Thread(w).start();
    }
}

结果:

这里如果不使用信号灯的话,输出的结果是一样的。

注意:wait方法和notify和同步一起使用的!!

六、任务调度

最后提一下Timer定时器

public class TimerDemo1{
    public static void main(String[] args) {
        Timer timer = new Timer();

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("哈哈....");
            }
        },new Date(System.currentTimeMillis()+1000), 2000);
    }

}

我们可以用Timer来实现任务定时,比如说上面的例子,1秒之后每隔2秒打印“哈哈....”同样还可以定其他的时间。


!!!好了,以上就是我对线程的一些相关的理解和梳理,重点就是Java中线程的创建、终止、还有线程的几种状态。。具体项目中用到的线程肯定不止这么简单,会涉及到线程池,任务调度相关的东西。未完待续。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章