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

JAVA基础再回首(二十四)——多线程的概述、实现方式、线程控制、生命周期、多线程程序练习、安全问题的解决

2016-09-30 15:56 1366 查看

JAVA基础再回首(二十四)——多线程的概述、实现方式、线程控制、生命周期、多线程程序练习、安全问题的解决

版权声明:转载必须注明本文转自程序员杜鹏程的博客:http://blog.csdn.net/m366917

这篇我们来学习多线程,java基础再回首也学了一段时间了,剩余的内容不多了,我们来继续学习。

多线程的概述

学习多线程,我们先来了解一下进程的概念

进程

正在运行的程序,是系统进行资源分配和调用的独立单位。

每一个进程都有它自己的内存空间和系统资源。

说起线程,它又分为单线程和多线程

线程

是进程中的单个顺序控制流,是一条执行路径

一个进程如果只有一条执行路径,则称为单线程程序

一个进程如果有多条执行路径,则称为多线程程序

多线程的实现(1)

如何实现多线程的程序呢?

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。

我们来通过查看API来学习多线程程序的实现



通过查看API,我们知道了有2中方式实现多线程程序。

方式1:继承Thread类

步骤

A:自定义类MyThread继承Thread类。

B:MyThread类里面重写run()

C:创建对象

D:启动线程

下面我们就自定义一个MyThread类继承Thread类启动线程

public class MyThread extends Thread {

@Override
public void run() {
// 自己写代码
// 一般来说,被线程执行的代码肯定是比较耗时的。所以我们用循环改进
for (int x = 0; x < 100; x++) {
System.out.println(x);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {

// 创建两个线程对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();

my1.start();
my2.start();
}
}


这样我们就创建并启动了两个线程

start()方法:首先启动了线程,然后再由jvm去调用该线程的run()方法。

那么,我们在继承Thread类之后,为什么要重写run()方法呢?

因为不是类中的所有代码都需要被线程执行的。而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。

获取和设置线程名称

Thread类的基本获取和设置方法

public final String getName():获取线程的名称。

public final void setName(String name):设置线程的名称

public class MyThread extends Thread {

public MyThread() {
}

public MyThread(String name){
super(name);
}

@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
// 创建线程对象
//无参构造+setXxx()
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//调用方法设置名称
my1.setName("阿杜");
my2.setName("杜鹏程");
my1.start();
my2.start();

//带参构造方法给线程起名字
// MyThread my1 = new MyThread("阿杜");
// MyThread my2 = new MyThread("杜鹏程");
// my1.start();
// my2.start();

//我们可以使用无参构造的方法,也可以使用带参构造的方法
}
}


但是我们要获取main方法所在的线程对象的名称,该怎么办呢?

遇到这种情况,Thread类提供了一个很好玩的方法:

public static Thread currentThread():返回当前正在执行的线程对象

System.out.println(Thread.currentThread().getName());

这句话如果在main中执行,就会输出main。会返回当前执行的线程对象

线程控制

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

public final void join():线程加入

public static void yield():线程礼让

public final void setDaemon(boolean on):后台线程

public final void stop():中断线程

public void interrupt():中断线程

我们来逐个学习

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

public class ThreadSleep extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x + ",日期:" + new Date());
// 睡眠1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadSleepDemo {
public static void main(String[] args) {
ThreadSleep ts1 = new ThreadSleep();
ThreadSleep ts2 = new ThreadSleep();

ts1.setName("阿杜");
ts2.setName("杜鹏程");

ts1.start();
ts2.start();
}
}


这样我们启动线程的时候就睡1秒执行一次

public final void join():线程加入,等待该线程终止

public class ThreadJoin extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}
public class ThreadJoinDemo {
public static void main(String[] args) {
ThreadJoin tj1 = new ThreadJoin();
ThreadJoin tj2 = new ThreadJoin();
ThreadJoin tj3 = new ThreadJoin();

tj1.setName("中秋节");
tj2.setName("国庆节");
tj3.setName("圣诞节");

tj1.start();
try {
tj1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

tj2.start();
tj3.start();
}
}


运行程序,我们发现名字为中秋节的线程走完了之后才开始走下面的两个线程。

给那个线程用这个方法就是等待该线程终止后,再继续执行接下来的线程。

public static void yield():线程礼让

public class ThreadYield extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
Thread.yield();
}
}
}
public class ThreadYieldDemo {
public static void main(String[] args) {
ThreadYield ty1 = new ThreadYield();
ThreadYield ty2 = new ThreadYield();

ty1.setName("阿杜");
ty2.setName("杜鹏程");

ty1.start();
ty2.start();
}
}


这个方法暂停当前正在执行的线程对象,并执行其他线程。

让多个线程的执行更和谐,但是不能靠它保证一人一次。

public final void setDaemon(boolean on):守护线程

public class ThreadDaemon extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}
public class ThreadDaemonDemo {
public static void main(String[] args) {
ThreadDaemon td1 = new ThreadDaemon();
ThreadDaemon td2 = new ThreadDaemon();

td1.setName("关羽");
td2.setName("张飞");

// 设置守护线程
td1.setDaemon(true);
td2.setDaemon(true);

td1.start();
td2.start();

Thread.currentThread().setName("刘备");
for (int x = 0; x < 5; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}


运行程序可以看到,当刘备执行完5次后,张飞和关于也会执行完,并不会执行100次。

将该线程标记为守护线程或用户线程。

当正在运行的线程都是守护线程时,Java 虚拟机退出。

该方法必须在启动线程前调用。

public final void stop():中断线程

public void interrupt():中断线程

这两个方法都是中断线程的意思,但是他们还是有区别的,我们来一起研究一下

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

// 我要休息10秒钟,亲,不要打扰我哦
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("线程被终止了");
}

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

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

// 你超过三秒不醒过来,我就干死你
try {
Thread.sleep(3000);
//           ts.stop();
ts.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


我们分别运行stop()方法和interrupt()方法。

可以看到一下运行结果





我们可以发现stop()方法执行后,该线程就停止了,不再继续执行了

但是interrupt()方法执行后,它会终止线程的状态,还会继续执行run方法里面的代码。

线程的生命周期图



多线程的实现(2)

方式2:实现Runnable接口

步骤:

A:自定义类MyRunnable实现Runnable接口

B:重写run()方法

C:创建MyRunnable类的对象

D:创建Thread类的对象,并把C步骤的对象作为构造参数传递

public class MyRunnable implements Runnable {

@Override
public void run() {
for (int x = 0; x < 100; x++) {
// 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();

// 创建Thread类的对象,并把C步骤的对象作为构造参数传递

// Thread(Runnable target, String name)
Thread t1 = new Thread(my, "阿杜");
Thread t2 = new Thread(my, "杜鹏程");

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


这样我们就实现了多线程的第二种启动方式

多线程程序练习

某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

我们分别用两种实现多线程的方法来完成这个需求

1.继承Thread类来实现

public class SellTicket extends Thread {

// 定义100张票
private static int tickets = 100;

@Override
public void run() {
// 定义100张票
// 每个线程进来都会走这里,这样的话,每个线程对象相当于买的是自己的那100张票,这不合理,所以应该定义到外面
// int tickets = 100;

// 是为了模拟一直有票
while (true) {
if (tickets > 0) {
System.out.println(getName() + "正在出售第" +(tickets--) + "张票");
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
// 创建三个线程对象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();

// 给线程对象起名字
st1.setName("窗口1");
st2.setName("窗口2");
st3.setName("窗口3");

// 启动线程
st1.start();
st2.start();
st3.start();
}
}


这样我们就实现了三个窗口同时在出售这100张票的多线程程序

2.实现Runnable接口的方式实现

public class SellTicket implements Runnable {
// 定义100张票
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();

// 创建三个线程对象
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毫秒

while (true) {
if (tickets > 0) {
// 为了模拟更真实的场景,我们稍作休息
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "张票");
}
}
}


这样我们模拟真是场景,稍作休息了,可是运行程序后,还是会出现下面两个问题。

相同的票出现多次

CPU的一次操作必须是原子性的

还出现了负数的票

随机性和延迟导致的

这里就牵扯到了线程的安全问题,线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。

多线程安全问题

如何解决多线程安全问题呢?

把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。

解决线程安全问题实现(1)

同步代码块

格式:

synchronized(对象){ 需要同步的代码; }

同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。

我们多上面售票的代码进行改进

public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象
private Object obj = new Object();

@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
}
}
}


我们只要运用同步代码块的格式来解决线程的问题就可以,主要就是这里的对象,必须使用的是同一个锁对象。

所以我们可以来总结一下同步的特点

同步的特点

同步的前提

多个线程

多个线程使用的是同一个锁对象

同步的好处

同步的出现解决了多线程的安全问题。

同步的弊端

当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

解决线程安全问题实现(2)

我们 还有一种方法可以解决多线程的安全问题

同步方法:就是把同步的关键字加到方法上

private synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}


我们只要调用这个方法就可以了

我们也可以让此方法为静态的方法

private static synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}


我们要来总结一下,同步代码块的锁对象可以时任意对象。

但是,当把同步关键字加在方法上,它的对象是this

当此方法为精态方法时,它的对象是类的字节码文件对象,也就是 类名.class

好了,多线程我们先学到这里,下篇我们继续学习。

欢迎有兴趣的同学加我朋友的QQ群:点击直接加群555974449 请备注:java基础再回首我们一起来玩吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 多线程 线程 安全
相关文章推荐