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

Java基础之多线程

2016-03-10 13:28 337 查看
一、多线程概述
1:要想了解多线程,必须先了解线程,而要想了解线程,必须先了解进程,因为线程是依赖于进程而存在。

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

3:多进程有什么意义呢?
单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。
举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。
也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。
并且呢,可以提高CPU的使用率。

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

4:什么是线程呢?
在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。
线程:是程序的执行单元,执行路径。是程序使用CPU的最基本单位。
单线程:如果程序只有一条执行路径。
多线程:如果程序有多条执行路径。

5:多线程有什么意义呢?
多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。
程序的执行其实都是在抢CPU的资源,CPU的执行权。
多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。

6.并发和并行
注意两个词汇的区别:并行和并发。
前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。
后者是物理上同时发生,指在某一个时间点同时运行多个程序。
6.Java程序运行原理
java 命令会启动 java 虚拟机,启动 JVM,等于启动了一个应用程序,
也就是启动了一个进程。该进程会自动启动一个 “主线程” ,
然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。
在此之前的所有程序都是单线程的。
思考:
jvm虚拟机的启动是单线程的还是多线程的?

  多线程的。

  原因是垃圾回收线程也要先启动,否则很容易会出现内存溢出。

  现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm的启动其实是多线程的。

二、多线程的实现方式
1.继承Thread类

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

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

  Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。
但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。

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

  然后提供一些类供我们使用。我们就可以实现多线程程序了。
那么Java提供的类是什么呢?

  Thread

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

方式1:继承Thread类。
步骤

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

  B:MyThread类里面重写run()?

  C:创建对象

  D:启动线程 调用start()方法
为什么要重写run()方法?
 不是类中的所有代码都需要被线程执行的。
 而这个时候,为了区分哪些代码能够被线程执行,
 java提供了Thread类中的run()用来包含那些被线程执行的代码。
为什么调用start()方法?
调用run()方法是单线程的,因为run()方法直接调用其实就相当于普通的方法调用,所以你看到的是单线程的效果
 
调用 start(),首先启动了线程,然后再由jvm去调用该线程的run()方法。

   //自定义线程类

public class MyThread extends Thread {

@Override
public void run() {

// 一般来说,被线程执行的代码肯定是比较耗时的。所以我们用循环改进
for (int x = 0; x < 500; x++) {
System.out.println(x);
}
}

}

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

// MyThread my = new MyThread();
// my.start();
//IllegalThreadStateException:非法的线程状态异常
// 因为这个相当于是my线程被调用了两次。而不是两个线程启动。
// my.start();

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

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


2.Thread类
(1)Thread类的基本获取和设置方法
public final String getName()
public final void setName(String name)
其实通过构造方法也可以给线程起名字
思考:
如何获取main方法所在的线程名称呢?
public static Thread currentThread()
这样就可以获取任意方法所在的线程名称
(2)线程调度
假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,
线程只有得到 CPU时间片,也就是使用权,才可以执行指令。
那么Java是如何对线程进行调用的呢?
线程有两种调度模型:
分时调度模型   所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型   优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。 
Java使用的是抢占式调度模型。
如何设置和获取线程优先级
public final int getPriority() :获取线程的优先级
public final void setPriority(int newPriority):设置线程的优先级
注意:
线程默认优先级是5。
线程优先级的范围是:1-10。
线程优先级高仅仅表示线程获取的 CPU时间片的几率高,
但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。
(3)线程控制
我们已经知道了线程的调度,接下来我们就可以使用如下方法对象线程进行控制
线程休眠
public static void sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
线程加入
public final void join():等待该线程终止。 当A线程执行到B线程的Join()方法是,A就会等待B线程都执行完,A才会执行
线程礼让
public static void yield():暂停当前正在执行的线程对象,并执行其他线程。 让多个线程的执行更和谐,但是不能靠它保证一人一次。
后台线程
public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。
当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。
中断线程
public final void stop():让线程停止,过时了,但是还可以使用。
public void interrupt():中断线程。 把线程的状态终止,并抛出一个InterruptedException。

3.多线程第二种方式:实现Runnable接口
步骤:
A:自定义类MyRunnable实现Runnable接口
B:重写run()方法
C:创建MyRunnable类的对象
D:创建Thread类的对象,并把实现Runnable接口的实现类对象作为构造参数传递

实现接口方式的好处
可以避免由于Java单继承带来的局限性。
适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。
在定义线程是,建议使用实现方式
为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。
调用Thread类中start方法启动线程。start方法会自动调用Runnable接口子类的run方法。

两种创建方式的区别
继承Thread:线程代码存放在Thread子类run方法中。
实现Runnable:线程代码存放在接口子类run方法中。

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();
}
}
三、线程的生命周期图
A:新建
B:就绪
C:运行
D:阻塞
E:死亡

//练习一:

/* 有100张票,而它有3个售票窗口售票,模拟该电影院售票   两种方式实现。
继承Thread类来实现。
*/

public class SellTicket extends Thread {

// 定义100张票
// private int tickets = 100;
// 为了让多个线程对象共享这100张票,我们其实应该用静态修饰
private static int tickets = 100;

@Override
public void run() {

// 是为了模拟一直有票
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();
}
}

//实现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();
}
}
四、线程安全问题
实现Runnable接口的方式实现,通过加入延迟后,就产生了两个问题:
A:相同的票卖了多次
  CPU的一次操作必须是原子性的
B:出现了负数票
  随机性和延迟导致的
线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。
问题的原因是:
当多条语句在操作同一个线程共享数据是,一个线程对多条语句只执行了一部分,还没有执行完,
另一个线程参与进来执行,导致共享数据的错误
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他的线程不可以参与执行
Java对于多线程的安全问题提供了专业的解决办法

1.同步代码块:

  synchronized(对象){

  需要同步的代码;

  }
对象是:可以是任意对象
需要同步的代码是:把多条语句操作共享数据的代码的部分给包起来
对象如同锁,持有锁的线程可以再同步中执行。
没有持有锁的线程及时获取到CPU的执行权,也进不去,因为没有获取锁
注意:
同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。

  多个线程必须是同一把锁。
举例:
火车上的卫生间。

同步的前提:
1.必须是要有两个或两个以上的线程。
2.必须保证同步中只能有一个线程在运行。
同步的好处 
同步的出现解决了多线程的安全问题。
同步的弊端

  当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
安全隐患,如何找问题
1.明确哪些代码是多线程运行代码
2.明确共享数据
3.明确多线程运行的代码中有那些语句是操作共享数据的
//加入同步,改进卖票练习
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--) + "张票");
}
}
}
}
}

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


2.同步方法
同步方法的格式及锁对象问题?

  把同步关键字加在方法声明上。
public synchronized void method(){
方法内容;
}

  同步方法是谁呢?

  函数需要被对象调用,那么函数都有一个所属对象引用,那就是this ,所以同步函数使用的锁是this

3.静态同步方法
静态方法及锁对象问题?

  通过验证发现不再是this ,因为静态方法中是不可以定义this的
静态进内存时,内存中没有本类对象,但是一定有该类的对应的字节码文件对象
比如: 类名.class   该对象的类型是Class
所以静态同步方法是用的锁是该方法所在类的字节码文件对象。类名.class
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: