java 多线程详解(多线程的创建、同步思想、死锁等)

线程和进程的概念

什么是进程?
进程就是正在运行的应用程序,是系统进行资源分配和调用的一个独立单位。每一个进程都有它自己的内存空间和系统资源。

一般来说,进程包括以下三个特征:
1、独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
2、动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中就扔了时间的概念。进程拥有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
3、并发性:多个进程可以在单个处理器上并发执行,多个进程间不会互相影响

多进程有什么意义?
单进程的计算机只能做一件事情。现在的计算机都支持多进程的。
多进程可以在一个时间段内执行多个任务,提高CPU的使用率。

一边玩游戏,一边听音乐是同时进行的吗?
不是。因为单CPU在某一个时间点上只能做一件事情。而我们在玩游戏,或者听音乐的时候,是CPU在做着程序间的高效切换,让我们以为它们是同时进行的。

什么是线程?
一个进程内可执行多个任务,这每一个任务就可以看成一个线程。一个进程有多个线程。线程是程序的执行单元,是一条执行路径。是程序使用CPU的最基本单位。
单线程程序:一个进程只有一条执行路径。
多线程程序:一个进程有多条执行路径。

多线程有什么意义?
不是提高程序的执行速度。是提高应用程序的使用率。
程序的执行其实都是在抢CPU的资源,CPU的执行权。多个线程都在抢这个资源,而如果其中的某一个进程的执行路径比较多,就会有更高的几率抢到CPU的执行权。
线程的执行具有随机性,因为我们不确定哪一个线程在哪一个时刻抢到这个资源。

并发和并行?
并行:逻辑上同时发生,指在某一个时间内同时运行多个程序。
并发:物理上同时发生,指在某一个时间点同时运行多个程序。

java程序运行原理是什么?
java命令会启动 java虚拟机(java virtual machine即JVM),JVM启动就相当于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个”主线程”,然后主线程去调用某个类的main方法。所以main方法运行在主线程中,在此之前的所有程序都是单线程的。

JVM虚拟机的启动时单线程还是多线程的?
多线程的。因为垃圾回收线程也要先启动,否则很容易出现内存溢出。垃圾回收线程和主线程,最低启动了两个线程,所以是多线程的。

多线程的优势:
当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源。但创建一个线程则简单的多。
进程之间不能共享数据,单线程之间共享内存非常容易。
系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
java语言内置了多线程功能的支持,而不是单纯地作为底层操作系统的调度方式,从而简化了java的多线程变成


多线程创建的四种方式

http://blog.csdn.net/qq_36748278/article/details/78143998


创建线程的几种方式的比较

为什么要重写run()方法?
因为不是类中所有的代码都要被线程执行,为了区分哪些代码能够被线程执行,Thread类中的run()方法只是包含那些被线程执行的代码。一般来说,被线程执行的代码是比较耗时的。

run()和start()的区别是什么?
run():仅仅是封装被线程执行的代码,直接调用仅仅是普通方法。
start():首先启动线程,然后由JVM去调用该线程的run()方法。

采用实现Runnable接口的方式有什么好处?
通过实现接口的方式解决了java单继承带来的局限性
适合多个相同的程序代码去处理同一个资源的情况,把线程同程序的代码以及数据进行有效分离,较好的体现了面向对象的设计思想。

public class MyThread extends Thread {
    private String myParam = null;
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.getName() + ":" + i);
        }
    }
}

MyThread myThread = new MyThread("这是第一个线程");
MyThread myThread2 = new MyThread("这是第二个线程");     //有几个就new几次

MyRunnable mr = new MyRunnable();             //只new了一次
Thread t1 = new Thread(mr,"小骨");
Thread t2 = new Thread(mr,"玥公子");

如果MyThread类中有别的成员变量,他就会创建很多次myParam变量。而通过实现接口的方式,他只new了一次,然后被重复利用,适合多个相同的程序代码去处理同一个资源的情况。

异常
IllegalThreadStateException:非法的线程状态异常。同一个线程不能被调用两次。需要创建两个线程。

方法
public final String getName():获取线程的名称
public final void setName(String name):设置线程的名称
public static Thread currentThread():返回当前正在执行的线程对象


线程调度

线程的两种调度模型?
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片多一些。
java采用的是抢占式调度模型。

线程的优先级?
线程优先级高仅仅表示线程获取的CPU时间片的几率高,但是要在次数比较多或者多次运行的情况下效果才明显。
public final int getPriority():获取线程对象的优先级。线程默认的优先级都是5
public final void setPriority(int newPriority):更改线程的优先级

public static final int MAX_PRIORITY:线程可以具有的最高优先级。值为10
public static final int NORM_PRIORITY:分配给线程的默认优先级。值为5
public static final int MIN_PRIORITY:线程可以具有的最低优先级。值为1

IllegalArgumentException异常:向方法传递了一个不合法或不正确的参数

线程类:

public class ThreadPriority extends Thread {
    @Override
    public void run() {
        for(int i = 0;i < 100;i++){
            System.out.println(getName() + ":" + i);
        }
    }
}

测试类:

public class ThreadPriorityDemo {
    public static void main(String[] args) {
        ThreadPriority tp = new ThreadPriority();
        ThreadPriority tp1 = new ThreadPriority();
        ThreadPriority tp2 = new ThreadPriority();

        tp.setName("老大");
        tp1.setName("老二");
        tp2.setName("老三");

        //获取默认优先级
        System.out.println(tp.getPriority());               //5
        System.out.println(tp1.getPriority());              //5
        System.out.println(tp2.getPriority());              //5

        System.out.println(Thread.MAX_PRIORITY);            //最高优先级:10
        System.out.println(Thread.MIN_PRIORITY);            //最低优先级:1
        System.out.println(Thread.NORM_PRIORITY);           //默认优先级:5       

        tp.start();
        tp1.start();
        tp2.start();
    }
}

线程控制

线程休眠: public static void sleep(long miliis)


线程加入: public final void join()。使用join()方法的线程中止之后才能执行后续的线程。
ThreadJoin类:

public class ThreadJoin extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":" + i);
        }
    }
}

测试类:

public class ThreadJoinDemo {
    public static void main(String[] args) {
        ThreadJoin ts = new ThreadJoin();
        ThreadJoin ts1 = new ThreadJoin();
        ThreadJoin ts2 = new ThreadJoin();

        ts.setName("爸爸");           // 有了爸爸,之后才可能有老大和老二
        ts1.setName("老大");
        ts2.setName("老二");

        ts.start();
        try {
            ts.join();              //老大和老二,必须等爸爸执行完了才能执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ts1.start();
        ts2.start();
    }
}



线程礼让: public static void yield()。
暂停当前线程对象,并执行其他线程。让多个线程执行更和谐,但是不能靠它保证一人一次。


后台线程: public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。守护线程是守护被守护的线程的,被守护的线程关闭之后,守护线程也要关闭。当正在运行的线程都是守护线程时,java虚拟机退出。该方法必须在启动前调用。
ThreadDaemon类:

public class ThreadDaemon extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":" + i);
        }
    }
}

测试类:

public class ThreadDaemonDemo {
    public static void main(String[] args) {
        ThreadDaemon ts = new ThreadDaemon();
        ThreadDaemon ts1 = new ThreadDaemon();

        ts.setName("左膀,守护将军");
        ts1.setName("右臂,守护将军");

        // 把ts和ts1都设置为守护线程,当主线程不存在的时候,他们两个守护线程也不能存在了
        ts.setDaemon(true);
        ts1.setDaemon(true);

        ts.start();
        ts1.start();

        Thread.currentThread().setName("将军"); // 将军不在了,左膀右臂也不能存在了
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);;
        }
    }
}



中断线程
public final void stop():让线程停止,已经过时了。具有不安全性,不建议使用
public void interrupt():中断线程,把线程状态终止,并抛出一个InterruptedException。也就是说走了异常,后面的代码还是会执行。
ThreadInterrupt类:

public class ThreadInterrupt extends Thread {
    @Override
    public void run() {
        System.out.println("开始执行:"+new Date());

        //休息10秒钟
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            System.out.println("线程被终止了");
        }

        System.out.println("结束执行:"+new Date());
    }
}

测试类:

public class ThreadInterruptDemo {
    public static void main(String[] args) {
        ThreadInterrupt ts = new ThreadInterrupt();
        ts.start();

        //超过3秒钟不醒过来,就结束线程
        try {
            Thread.sleep(3000);
            ts.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

控制台输出:

开始执行:Sun Sep 24 15:29:51 CST 2017
线程被终止了
结束执行:Sun Sep 24 15:29:54 CST 2017

线程的生命周期

线程的一些状态:
1、新建:创建线程对象
2、就绪:线程有执行资格,没有执行权
3、运行:有执行资格,有执行权
4、阻塞:由于一些操作让线程改变了状态,没有执行资格,没有执行权
另一些操作可以把它给激活,激活处于就绪状态
5、死亡:线程对象变成垃圾,等待被回收
这里写图片描述


用多线程实现卖电影票实例

通过使用Runnable接口的方式:
SellTicket类:

public class SellTicket implements Runnable {
    private int tickets = 100;          //通过实现接口的方式,这个类只会被创建一次,因此这个变量也是被共享的。

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
            }
        }
    }
}

测试类:

public class SellTicketDemo {
    public static void main(String[] args) {
        //创建资源对象
        SellTicket st = new SellTicket();

        //创建3个线程对象
        Thread t1 = new Thread(st,"窗口1");
        Thread t2 = new Thread(st,"窗口2");
        Thread t3 = new Thread(st,"窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}



在真实生活中,售票时网络是不能实时传输的,总是存在延迟的情况,所以,在出售一张票以后,需要一点时间的延迟。因此我们采用每次卖票延迟100毫秒来模拟真实的情况:
修改SellTicket类:

public class SellTicket implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                //为了模拟更真实的场景,在这里sleep:100ms
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
            }
        }
    }
}

控制台输出错误事例情况之一:只贴出了错误信息:

窗口2正在出售第1张票
窗口3正在出售第0张票
窗口1正在出售第-1张票


通过加入延迟后,就发生了两个问题:
1、相同的票卖了多次:CPU的每一次执行必须是一个原子性(最简单基本的)操作。
(1)假设此时tickets=100
(2)因为当A线程进入了if(tickets>0)之后,它会睡眠
(3)此时B线程进来了,它进入了if(tickets>0),之后它也睡眠
(4)然后A线程醒了,它卖了第100张票。
(5)CPU的每一次执行必须是一个原子性(最简单基本的)操作。但是tickets– 并不是原子性操作。
         a:它先记录以前的值
         b:然后tickets–
         c:然后输出以前的值
         d:tickets的值变成99。
(6)如果上面的操作一次性做完,是正常的,但是如果上面的操作做了一半,然后B线程醒了,此时tickets还没变成99,那么也就是说B线程也卖了第100张票。因此就出现了相同的票卖了多次的现象。
2、出现了负票
(1)假设现在tickets = 1
(2)A线程进来了if(tickets > 0)并休息,B线程也进来了也休息,C线程也进来了也休息。
(3)A 正在出售第1张票,tickets = 0;
         B 正在出售第0张票,tickets = -1;
         C 正在出售第-1张票,tickets = -2;

这就是我们所说的线程安全问题。下面我将会讲解如何解决这些线程安全问题。


线程安全问题

在什么情况下会出现线程安全问题?当同时满足以下三个情况的时候,我们的程序就会出现问题。
(1)是否是多线程环境?是
(2)是否有共享数据?是
(3)是否有多条语句操作共享数据?是

如何解决多线程安全问题?
基本思想:让程序没有安全问题的环境。
也就是说把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。

多线程安全是什么?
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。


同步解决线程安全问题:
方式一、同步代码块:同步代码块的锁对象是任意对象。多个线程的锁必须是一把锁。这个对象也必须使用公用的。
synchronized(任意对象){
       需要同步的代码
}
卖电影票的解决方法:

public class SellTicket implements Runnable {
    private int tickets = 100;

    //创建锁对象。(保证所有的线程都是用的同一把锁)
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {

            //同步代码块
            synchronized (obj) {
                if (tickets > 0) {
                    //为了模拟更真实的场景,在这里sleep:100ms
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                }
            }
        }
    }
}


方式二、同步方法:把同步关键字加在方法上。锁对象是this
卖电影票的解决方法:

public class SellTicket implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if(tickets%2 == 0){
                //可以知道,同步方法的锁对象是this
                synchronized (this) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                    }
                }
            }else{
                sellTicket();
            }
        }
    }   

    private synchronized void sellTicket(){
        if (tickets > 0) {
            //为了模拟更真实的场景,在这里sleep:100ms
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
        }
    }
}


方式三、静态方法:把同步关键字加在方法上。锁对象是类的字节码文件
卖电影票的解决方法:

public class SellTicket implements Runnable {
    private static int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if(tickets%2 == 0){
                //可以知道,静态方法的锁对象是类的字节码文件对象
                synchronized (SellTicket.class) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                    }
                }
            }else{
                sellTicket();
            }
        }
    }


    private static synchronized void sellTicket(){
        if (tickets > 0) {
            //为了模拟更真实的场景,在这里sleep:100ms
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
        }
    }
}

JDK5之后的Lock锁。(一般情况下使用synchronized)

虽然我们可以通过同步代码块和同步方法添加锁对象,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

但是我们可以发现:Lock锁对象其实是一个接口。
java.util.concurrent.locks :接口 Lock
所有已知实现类: ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock


下面还是用卖票的例子来看看Lock对象是怎么使用的:
SellTicket类:

public class SellTicket implements Runnable {
    private int tickets = 100;

    // 定义锁对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {

            try {
                // 加锁
                lock.lock();
                if (tickets > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets-- + "张票");
                }
            } finally {
                // 释放锁(放在try-finally中:防止前面出问题之后,就没办法释放锁。通过放在finally之后,无论发生什么情况都会释放锁)
                lock.unlock();
            }
        }
    }
}

测试类:

public class SellTicketDemo {
    public static void main(String[] args) {
        SellTicket st = new SellTicket();

        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

同步思想

同步的前提:有多个线程,并且多个线程使用的是同一个锁对象。
同步的好处:同步的出现解决了多线程的安全问题
同步的弊端:
(1)当线程非常多的时候,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
(2)容易产生死锁。


死锁问题

死锁:两个或两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象。

Lock类:创建两个锁对象

public class MyLock {
    //创建两把锁对象
    public static final Object objA = new Object();
    public static final Object objB = new Object(); 
}

死锁代码:MyLock类

public class DieLock extends Thread {
    private boolean flag;

    public DieLock(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (MyLock.objA) {
                System.out.println("if objA");
                synchronized (MyLock.objB) {
                    System.out.println("if objB");
                }
            }
        }else{
            synchronized (MyLock.objB) {
                System.out.println("else objB");
                synchronized (MyLock.objA) {
                    System.out.println("else objA");
                }
            }
        }
    }
}

测试类:

public class DieLockDemo {
    public static void main(String[] args) {
        DieLock dl1 = new DieLock(true);
        DieLock dl2 = new DieLock(false);

        dl1.start();
        dl2.start();
    }
}

控制台输出:我们可以发现出现了互相等待的情况,也就是死锁现象

else objB
if objA

常用的线程安全的类

线程安全的类(都有synchronized修饰,线程安全,效率低)
StringBuffer
Vector
Hashtable

如何把一个线程不安全的集合类变成一个线程安全的集合类:
public static List synchronizedList(List list)
List list1 = Collections.synchronizedList(new ArrayList());

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值