您的位置:首页 > 编程语言 > Java开发

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

2017-09-24 09:51 645 查看

线程和进程的概念

什么是进程?

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

一般来说,进程包括以下三个特征:

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());
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐