您的位置:首页 > 职场人生

黑马程序员--Java多线程

2015-08-30 17:19 796 查看
——- android培训java培训、期待与您交流! ———-

进程与线程

要想了解多线程,必须先了解线程,而要想了解线程,必须先了解进程,因为线程是依赖于进程而存在。

什么是进程

通过任务管理器我们就看到了进程的存在。

而通过观察,我们发现只有运行的程序才会出现进程。

进程:就是正在运行的程序。

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

多进程的意义

单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。

举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。

也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。

并且呢,可以提高CPU的使用率。

一边打游戏,一边听音乐,不是同时进行的,单CPU在某一时间点上只能做一件事。

CPU在做程序间的高效切换让我们感觉打游戏和听音乐是同时进行的。

什么是线程

在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。

线程:是程序的执行单元,执行路径。是程序使用CPU的最基本单位。

单线程:如果程序只有一条执行路径。

多线程:如果程序有多条执行路径。

多线程的意义

多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。

程序的执行其实都是在抢CPU的资源,CPU的执行权。

多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。

我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。

并行和并发

前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。

后者是物理上同时发生,指在某一个时间点同时运行多个程序。

Java程序的运行原理

由java命令启动JVM,JVM启动就相当于启动了一个进程。

接着由该进程创建了一个主线程去调用main方法。

JAVA虚拟机的启动是多线程的,除了主线程,至少还需要垃圾回收线程,否则内存很快就会溢出。

如何实现多线程

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。

而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。

Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。

但是Java可以去调用C/C++写好的程序来实现多线程程序。

由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,

然后提供一些类供我们使用。我们就可以实现多线程程序了。

方式一:继承Thread类

首先自定义一个继承Thread的方法

public class MyThread extends Thread {

@Override
public void run() {
for (int x = 0; x < 200; x++) {
System.out.println(x);
}
}
}


重写run()方法的作用

不是类中的所有代码都需要被线程执行的。

为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。

创建线程对象测试

public class MyThreadDemo {
public static void main(String[] args) {

// 创建两个线程对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//my1.run();//这是单线程

//启动线程
my1.start();
my2.start();
}
}


调用run()方法为什么是单线程?

因为run()方法直接调用其实就相当于普通的方法调用,所以你看到的是单线程的效果

run()和start()的区别?

run():仅仅是封装被线程执行的代码,直接调用是普通方法

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

获取和设置线程对象的名称

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

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

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());
}
}


线程调度

我们知道,一般我们的计算机只有一个CPU,而CPU在某一个时刻只能运行一条指令,线程只有得到CPU的使用权,才能执行线程,那么Java是如何对线程进行调度的呢?

线程有两种调度模型:

1.分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。

2.抢占式调度模型,优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取CPU时间片的几率稍大。

Java使用的是抢占式调度模型

设置和获取线程的优先级

public final int getPriority():返回线程对象的优先级

public final void setPriority(int newPriority):更改线程的优先级。

注意:

程默认优先级是5。

优先级的范围是:1-10。

先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。

public class ThreadPriorityDemo {
public static void main(String[] args) {
//创建线程对象
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();

//设置对象名称
tp1.setName("徐凤年");
tp2.setName("李淳罡");
tp3.setName("黄阵图");

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

// 设置线程优先级
// tp1.setPriority(100000);//报错

//设置正确的线程优先级
tp1.setPriority(10);
tp2.setPriority(1);

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


线程休眠

重写run()方法,加入休眠时间

import java.util.Date;
/*
* 线程休眠
*      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());
// 设置休眠时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}


加入线程

/*
* public final void join():等待该线程终止。
* 别的线程需要等待这个加入的线程执行结束才能执行。
*/
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 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):将该线程标记为守护线程或用户线程。
* 当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。
*
*/
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);
}
}
}


中断线程

/*
* 中断线程:
*                 public void interrupt()
*/

public class TreadDemo {
public static void main(String[] args) {
// 创建线程
MyThread m1 = new MyThread();
MyThread m2 = new MyThread();

// 设置线程名称
m1.setName("黑马");
m2.setName("白马");

// 中断线程,后面的线程还可以继续运行
m1.interrupt();
// 启动线程
m2.start();
}
}


线程的生命周期



方式二:实现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(Runnable target, String name)
Thread t1 = new Thread(my, "林青霞");
Thread t2 = new Thread(my, "刘意");

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


已有方式一,为何会有方式二出现?

1.可以避免由于java单继承带来的局限性

2.适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码,数据有效分离,较好的体现了面向对象的设计思想。

方式三:实现Callable接口

需要和线程池结合使用

实现类

import java.util.concurrent.Callable;

/*
* Callable<V>:带泛型的接口
*                 接口中只有一个方法:V call()
*                 接口中的泛型是call()方法的返回值类型
*
*/
public class MyCallable implements Callable {

@Override
public Object call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
return null;
}
}


测试类

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyCallableDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);

// 添加Callable实现类
pool.submit(new MyCallable());
pool.submit(new MyCallable());

// 结束线程池
pool.shutdown();
}
}


线程安全

解决线程安全的基本思想(判断线程是否有问题的标准)

是否是多线程环境

是否有共享数据

是否有多条语句操作共享数据

电影票案例

public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;

@Override
public void run() {
while (true) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

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();
}
}


这样会出现以下问题

1.相同的票卖了多次

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

2.出现负数票

线程的随机性和延迟导致的

线程安全的解决方式

解决方式一:同步代码块

/*
* synchronized(对象){
*                 代码;
* }
*
* 注意:同步代码块可以解决安全问题的根本原因在对象上,该对象如果锁一样的功能,别的线程不能进入。
*                  这个对象可以是任意对象,最好是用本身this作为这个对象。
*
*/
public class SellTicekt implements Runnable {
private int ticket = 100;

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


解决方式二:同步方法

/*
* synchronized关键字修饰方法
* 锁对象是this
*/
public class SellTicekt implements Runnable {
private int ticket = 100;

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


同步的特点:

前提:多个线程

解决问题的时候要注意:多个线程使用的是同一个锁对象

同步的好处

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

同步的弊端

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

Lock锁

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "正在出售第" + (tickets--) + "张票");
}
} finally {
// 释放锁
lock.unlock();
}
}
}

}


死锁

两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象。

举例:

中国人,美国人吃饭案例。

正常情况:

中国人:筷子两支

美国人:刀和叉

现在:

中国人:筷子1支,刀一把

美国人:筷子1支,叉一把

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


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();
}
}


线程的状态转换图及常见执行情况



线程间通信

指不同种类的线程针对同一资源的操作

资源类

/*
* 定义学生类
*/
public class Student {
String name;
int age;
boolean flag;// 用来判断是否存在资源,默认是flash,没有资源

public synchronized void set(String name, int age) {
// 生产者,如果有数据就等待
if (!this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 设置数据
this.name = name;
this.age = age;
// 修改标记
this.flag = false;
// 唤醒线程
this.notify();
}

public synchronized void get() {
if (this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.name + ":" + this.age);
// 修改标记
this.flag = true;
// 唤醒线程
this.notify();
}
}


设置类

/*
* 设置学生信息的线程
*/
public class SetThread implements Runnable {

private Student s;
private int i;

public SetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
if (i % 2 == 0) {
s.set("小明", 5);
} else {
s.set("汪汪", 2);
}
i++;
}
}
}


获取类

/*
* 设置获取学生信息的线程
*/
public class GetThread implements Runnable {

private Student s;

public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
s.get();
}
}
}


测试类

public class StudentDemo {
public static void main(String[] args) {
// 创建资源
Student s = new Student();

// 创建SetThread和GetThread对象
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);

// 创建线程
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);

// 开启线程
t1.start();
t2.start();
}
}


这样线程安全是解决了,但是还存在着以下问题。

1.如果消费者先抢到CPU执行权,消费数据,这时数据如果是空,就没有意义。

应该等着数据生产出来,再去消费,这样才具有意义。

2.如果生产者先抢到CPU执行权,生产数据,但是生产完一定数量的数据以后,还继续持有执行权,

它还会继续生产数据,这还现实情况不符,需要等着消费者把数据消费以后,再生产。

正常思路:

1.生产者

先看是否有数据,有就等待,没有就生产,生产完通知消费者消费

2.消费者

先看是否有数据,有就消费,没有就等待,消费完通知生产者生产

java提供了一个等待唤醒机制来解决这个问题。



Object类中提供了三个方法:

wait():等待

notify():唤醒单个线程

为什么等待唤醒方法定义在Object类中:

这些方法都是通过锁对象进行调用的,锁对象可以是任意的

所以,这些方法必须定义在Object类中。

测试类

public class StudentDemo {
public static void main(String[] args) {
//创建资源
Student s = new Student();

//设置和获取的类
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);

//线程类
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);

//启动线程
t1.start();
t2.start();
}
}


资源类

public class Student {
String name;
int age;
boolean flag;
}


生产者类

public class GetThread implements Runnable {
private Student s;

public GetThread(Student s) {
this.s = s;
}

@Override
public void run() {
while (true) {
synchronized (s) {
if(!s.flag){
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.println(s.name + "---" + s.age);

//修改标记
s.flag = false;
//唤醒线程
s.notify();
}
}
}
}


消费者类

public class SetThread implements Runnable {

private Student s;
private int x = 0;

public SetThread(Student s) {
this.s = s;
}

@Override
public void run() {
while (true) {
synchronized (s) {
//判断有没有
if(s.flag){
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

if (x % 2 == 0) {
s.name = "李延旭";
s.age = 21;
} else {
s.name = "黑马";
s.age = 22;
}
x++; //x=1

//修改标记
s.flag = true;
//唤醒线程
s.notify(); //唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。
}
}
}
}


总结

多线程的实现有三个方法,我们常用的是第二种,所以第二种是要必须掌握的,其他两种了解即可。对于线程的状态转换和执行图,也是必须要理解的,这样有利于学习多线程。等待唤醒机制,是很符合现实的生活一个机制,需要熟练掌握。

——- android培训java培训、期待与您交流! ———-
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: