-----------android培训、java培训、java学习型技术博客、期待与您交流!------------
一、多线程概述
(一)多线程引入
程序只有一个执行流程,这样的程序就是单线程程序。
假如一个程序有多条执行流程,那么,该程序就是多线程程序。
(二)多线程概述
进程:
正在运行的程序,是系统进行资源分配和调用的独立单位。
每一个进程都有它自己的内存空间和系统资源。
线程:
是进程中的单个顺序控制流,是一条执行路径
一个进程如果只有一条执行路径,则称为单线程程序。
一个进程如果有多条执行路径,则称为多线程程序。
进程与线程的关系:
线程是依赖于进程存在的,线程就是一个进程中子程序
1、多进程有什么意义呢?
单进程计算机只能做一件事情。而我们现在的计算机都可以一边玩游戏(游戏进程),一边听音乐(音乐进程),所以我们常见的操作系统都是多进程操作系统。比如:Windows,Mac和Linux等,能在同一个时间段内执行多个任务。
对于单核计算机来讲,游戏进程和音乐进程是同时运行的吗?不是。
因为CPU在某个时间点上只能做一件事情,计算机是在游戏进程和音乐进程间做着频繁切换,且切换速度很快,所以,我们感觉游戏和音乐在同时进行,其实并不是同时执行的。
多进程的作用不是提高执行速度,而是提高CPU的使用率。
2、多线程有什么意义呢?
多线程的作用不是提高执行速度,而是为了提高应用程序的使用率。
而多线程却给了我们一个错觉:让我们认为多个线程是并发执行的。其实不是。
因为多个线程共享同一个进程的资源(堆内存和方法区),但是栈内存是独立的,一个线程一个栈。所以他们仍然是在抢CPU的资源执行。一个时间点上只有能有一个线程执行。而且谁抢到,这个不一定,所以,造成了线程运行的随机性。
3、那么什么又是并发呢?
大家注意两个词汇的区别:并行和并发。
前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。
后者是物理上同时发生,指在某一个时间点同时运行多个程序。
那么,我们能不能实现真正意义上的并发呢,是可以的,多个CPU就可以实现,不过你得知道如何调度和控制它们。
(三)Java程序运行原理
java 命令会启动 java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。在此之前的所有程序都是单线程的。
二、多线程的实现方案1
(一)步骤
继承Thread
重写run
创建子类对象
调用start()
(二)代码演示
package thread;
public class ThreadDemo {
public static void main(String[] args) {
CreateThread thtead = new CreateThread();
thtead.start();
for (int i = 0; i < 5; i++) {
System.out.println("main--"+i);
}
}
}
class CreateThread extends Thread{
public void run(){
for (int i = 0; i <5; i++) {
System.out.println("run--"+i);
}
}
}
运行结果:
注意:每次运行的结果是不一样的,这是由于多线程的随机性,无法控制,这个是由CPU决定的。
三、如何获取和设置线程名称
Thread类的基本获取和设置方法
public final String getName():获取线程名字
public final void setName(String name):设置线程名字
其实通过构造方法也可以给线程起名字,直接在创建时传参
思考:
如何获取main方法所在的线程名称呢?
public static Thread currentThread():这样就可以获取任意方法所在的线程名称
代码演示:
package thread;
public class ThreadGet_Set {
public static void main(String[] args) {
ThreadName tmg = new ThreadName();
ThreadName tms = new ThreadName("线程");
tmg.start();
//获取当前正在执行的线程名字
String nametmg = Thread.currentThread().getName();
System.out.println(nametmg);
//设置线程名字
tms.setName("线程");
tms.start();
}
}
class ThreadName extends Thread{
//构造方法
public ThreadName(){}
public ThreadName(String name) {
super(name);
}
public void run(){
//获取线程名字
System.out.println(getName());
}
}
四、线程调度
假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?
线程有两种调度模型:
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java使用的是抢占式调度模型。
设置和获取线程优先级方法
public final int getPriority()
public final void setPriority(int newPriority)
代码演示
package thread;
public class ThreadPriority{
public static void main(String[] args) {
Priority p = new Priority();
Priority p2 = new Priority();
int num = p.getPriority();
System.out.println("默认:"+num);
//设置优先级
p.setPriority(Thread.MIN_PRIORITY);
p2.setPriority(Thread.MAX_PRIORITY);
p.start();
p2.start();
}
}
class Priority extends Thread{
public void run(){
for (int i = 0; i <3; i++) {
System.out.println(getName()+"run--"+i);
}
}
}
五、线程控制
(一)线程控制方法
1、线程休眠
public static void sleep(long millis):让线程在指定毫秒值内,休眠,时间一到,自动醒来继续执行
代码演示
package thread;
public class ThreadSleep {
public static void main(String[] args) {
new Sleep().start();
}
}
class Sleep extends Thread{
public void run(){
for (int i = 0; i < 3; i++) {
//调用方法
try{
Thread.sleep(2000);
}catch(InterruptedException ex){}
System.out.println("run--"+i);
}
}
}
2、线程加入
public final void join()
3、线程礼让
public static void yield()
4、后台线程
public final void setDaemon(boolean on)
代码演示
package thread;
/*
* Thread 类方法setDaemon(boolean b)
* 将线程设置为后台线程,传递是true,设置为后台线程
* 守护线程
* 程序中所有的线程都是守护线程的时候,JVM就自动退出了
* setDaemon()方法必须在start()方法之前
*/
public class ThreadSetDaemon {
public static void main(String[] args) {
SetDaemon sd = new SetDaemon();
sd.setDaemon(true);
sd.start();
}
}
class SetDaemon extends Thread{
public void run(){
while (true) {
System.out.println("run--");
}
}
}
5、中断线程
public final void stop()
public void interrupt()
六、线程的生命周期图
七、多线程的实现方案2
实现Runnable接口
如何获取线程名称
如何给线程设置名称
实现接口方式的好处
可以避免由于Java单继承带来的局限性。
适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。
代码演示
package thread;
/*
* 创建新线程第二种方式,实现接口方式
* 对售票案例,进行改进
* 延迟处理,出现错误的票数
* 同步技术,保证线程安全性,同步代码块
*
* 速度降低,数据安全
*/
class RunnableTicket implements Runnable {
private int tickets = 100;
private Object obj = new Object();
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (Exception ex) {
}
System.out.println(Thread.currentThread().getName()
+ " 出售第" + tickets--);
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
RunnableTicket r = new RunnableTicket();
Thread t0 = new Thread(r);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t0.start();
t1.start();
t2.start();
}
}
八、两种方式的区别和线程的几种状态
1、两种创建方式的区别
继承Thread:线程代码存放在Thread子类run方法中。
实现Runnable:线程代码存放在接口子类run方法中。
2、几种状态
被创建:等待启动,调用start启动。
运行状态:具有执行资格和执行权。
临时状态(阻塞):有执行资格,但是没有执行权。
冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行资格,变为临时状态。
消忙状态:stop()方法,或者run方法结束。
注:当已经从创建状态到了运行状态,再次调用start()方法时,就失去意义了,java运行时会提示线程状态异常。
3、图解:
九、线程安全问题
1、导致安全问题的出现的原因:
当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没用执行完,另一个线程参与进来执行。导致共享数据的错误。
简单的说就两点:
a、多个线程访问出现延迟。
b、线程随机性 。
注:线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。
2、解决办法——同步
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
在java中对于多线程的安全问题提供了专业的解决方式——synchronized(同步)
这里也有两种解决方式,一种是同步代码块,还有就是同步函数。都是利用关键字synchronized来实现。
a、同步代码块
用法:
synchronized(对象)
{需要被同步的代码}
同步可以解决安全问题的根本原因就在那个对象上。其中对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
示例:
package thread;
/*
* 模拟储户同时在多个窗口存钱
* 账户余额0
* 2个窗口同时存钱,每个窗口存3次,每次存储100元
* 1 2 3 4 5 6
*
* 存钱功能,银行的还是储户的,储户调用银行方法存钱,参数传递
*
* saveMoney 方法所有的代码,都是线程操作共享数据
* 同步代码块,结构比较负责,能不能同步整个方法
* 同步关键字写在方法上,返回值类型前
*
* 非静态同步方法的锁,是this本类对象引用
* 静态方法同步的锁,也是一个对象,本类名字.class
* 每个数据类型,JVM赋予静态属性,属性名字class
* 属性获取到class文件对象
*/
class Bank{
private static int sum = 0 ;
public static synchronized void saveMoney(int money){
//synchronized(Bank.class){
sum = sum + money;
System.out.println(sum);
//}
}
}
//定义储户类,同时调用银行的存钱功能
class Customer implements Runnable{
private Bank b = new Bank();
public void run(){
for(int x = 0 ; x < 3 ; x++){
b.saveMoney(100);
}
}
}
public class ThreadDemo{
public static void main(String[] args) {
Customer c = new Customer();
Thread t0 = new Thread(c);
Thread t1 = new Thread(c);
t0.start();
t1.start();
}
}
3、同步的前提
a,必须要有两个或者两个以上的线程。
b,必须是多个线程使用同一个锁。
4、同步的利弊
好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源。
5、如何寻找多线程中的安全问题
a,明确哪些代码是多线程运行代码。
b,明确共享数据。
c,明确多线程运行代码中哪些语句是操作共享数据的。
十、静态函数的同步方式
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。如:
类名.class 该对象的类型是Class
这就是静态函数所使用的锁。而静态的同步方法,使用的锁是该方法所在类的字节码文件对象。类名.class
经典示例:
package thread;
/*
加同步的单例设计模式————懒汉式
*/
class Single {
private static Single s = null;
private Single() {
}
public static void getInstance() {
if (s == null) {
synchronized (Single.class) {
if (s == null)
s = new Single();
}
}
return s;
}
}
十一、死锁
当同步中嵌套同步时,就有可能出现死锁现象。
示例:
package thread;
/*
写一个死锁程序
*/
//定义一个类来实现Runnable,并复写run方法
class LockTest implements Runnable {
private boolean flag;
LockTest(boolean flag) {
this.flag = flag;
}
public void run() {
if (flag) {
while (true) {
synchronized (LockClass.locka)// a锁
{
System.out.println(Thread.currentThread().getName()
+ "------if_locka");
synchronized (LockClass.lockb)// b锁
{
System.out.println(Thread.currentThread().getName()
+ "------if_lockb");
}
}
}
} else {
while (true) {
synchronized (LockClass.lockb)// b锁
{
System.out.println(Thread.currentThread().getName()
+ "------else_lockb");
synchronized (LockClass.locka)// a锁
{
System.out.println(Thread.currentThread().getName()
+ "------else_locka");
}
}
}
}
}
}
// 定义两个锁
class LockClass {
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLock {
public static void main(String[] args) {
// 创建2个进程,并启动
new Thread(new LockTest(true)).start();
new Thread(new LockTest(false)).start();
}
}
结果:程序卡住,不能继续执行
十二、线程间通信
其实就是多个线程在操作同一个资源,但是操作的动作不同。
1、使用同步操作同一资源的示例:
package thread;
/*
有一个资源
一个线程往里存东西,如果里边没有的话
一个线程往里取东西,如果里面有得话
*/
//资源
class Resource {
private String name;
private String sex;
private boolean flag = false;
public synchronized void setInput(String name, String sex) {
if (flag) {
try {
wait();
} catch (Exception e) {
}// 如果有资源时,等待资源取出
}
this.name = name;
this.sex = sex;
flag = true;// 表示有资源
notify();// 唤醒等待
}
public synchronized void getOutput() {
if (!flag) {
try {
wait();
} catch (Exception e) {
}// 如果木有资源,等待存入资源
}
System.out.println("name=" + name + "---sex=" + sex);// 这里用打印表示取出
flag = false;// 资源已取出
notify();// 唤醒等待
}
}
// 存线程
class Input implements Runnable {
private Resource r;
Input(Resource r) {
this.r = r;
}
public void run()// 复写run方法
{
int x = 0;
while (true) {
if (x == 0)// 交替打印张三和王羲之
{
r.setInput("张三", ".....man");
} else {
r.setInput("王羲之", "..woman");
}
x = (x + 1) % 2;// 控制交替打印
}
}
}
// 取线程
class Output implements Runnable {
private Resource r;
Output(Resource r) {
this.r = r;
}
public void run()// 复写run方法
{
while (true) {
r.getOutput();
}
}
}
class ResourceDemo2 {
public static void main(String[] args) {
Resource r = new Resource();// 表示操作的是同一个资源
new Thread(new Input(r)).start();// 开启存线程
new Thread(new Output(r)).start();// 开启取线程
}
}
结果:部分截图
几个小问题:
1)wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?
a,这些方法存在与同步中。
b,使用这些方法时必须要标识所属的同步的锁。同一个锁上wait的线程,只可以被同一个锁上的notify唤醒。
c,锁可以是任意对象,所以任意对象调用的方法一定定义Object类中。
2)wait(),sleep()有什么区别?
wait():释放cpu执行权,释放锁。
sleep():释放cpu执行权,不释放锁。
3)为甚么要定义notifyAll?
因为在需要唤醒对方线程时。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所以线程都等待。
2、JDK1.5中提供了多线程升级解决方案。
将同步synchronized替换成显示的Lock操作。将Object中wait,notify,notifyAll,替换成了Condition对象。该Condition对象可以通过Lock锁进行获取,并支持多个相关的Condition对象。
升级解决方案的示例:
package thread;
/*
生产者生产商品,供消费者使用
有两个或者多个生产者,生产一次就等待消费一次
有两个或者多个消费者,等待生产者生产一次就消费掉
*/
import java.util.concurrent.locks.*;
class Resource {
private String name;
private int count = 1;
private boolean flag = false;
// 多态
private Lock lock = new ReentrantLock();
// 创建两Condition对象,分别来控制等待或唤醒本方和对方线程
Condition condition_pro = lock.newCondition();
Condition condition_con = lock.newCondition();
// p1、p2共享此方法
public void setProducer(String name) throws InterruptedException {
lock.lock();// 锁
try {
while (flag)
// 重复判断标识,确认是否生产
condition_pro.await();// 本方等待
this.name = name + "......" + count++;// 生产
System.out.println(Thread.currentThread().getName() + "...生产..."
+ this.name);// 打印生产
flag = true;// 控制生产\消费标识
condition_con.signal();// 唤醒对方
} finally {
lock.unlock();// 解锁,这个动作一定执行
}
}
// c1、c2共享此方法
public void getConsumer() throws InterruptedException {
lock.lock();
try {
while (!flag)
// 重复判断标识,确认是否可以消费
condition_con.await();
System.out.println(Thread.currentThread().getName() + ".消费."
+ this.name);// 打印消费
flag = false;// 控制生产\消费标识
condition_pro.signal();
} finally {
lock.unlock();
}
}
}
// 生产者线程
class Producer implements Runnable {
private Resource res;
Producer(Resource res) {
this.res = res;
}
// 复写run方法
public void run() {
while (true) {
try {
res.setProducer("商品");
} catch (InterruptedException e) {
}
}
}
}
// 消费者线程
class Consumer implements Runnable {
private Resource res;
Consumer(Resource res) {
this.res = res;
}
// 复写run
public void run() {
while (true) {
try {
res.getConsumer();
} catch (InterruptedException e) {
}
}
}
}
class ProducerConsumer {
public static void main(String[] args) {
Resource res = new Resource();
new Thread(new Producer(res)).start();// 第一个生产线程 p1
new Thread(new Consumer(res)).start();// 第一个消费线程 c1
new Thread(new Producer(res)).start();// 第二个生产线程 p2
new Thread(new Consumer(res)).start();// 第二个消费线程 c2
}
}
运行结果:部分截图
十三、停止线程
在JDK 1.5版本之前,有stop停止线程的方法,但升级之后,此方法已经过时。
那么现在我们该如果停止线程呢?
只有一种办法,那就是让run方法结束。
1、开启多线程运行,运行代码通常是循环结构。只要控制住循环,就可以让run方法结束,也就是线程结束。
如:run方法中有如下代码,设置一个flag标记。
public void run()
{
while(flag)
{
System.out.println(Thread.currentThread().getName()+"....run");
}
}
那么只要在主函数或者其他线程中,在该线程执行一段时间后,将标记flag赋值false,该run方法就会结束,线程也就停止了。
2、上面的1方法可以解决一般情况,但是有一种特殊情况:就是当线程处于冻结状态。就不会读取到标记。那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。Thread类提供该方法interrupt();
如:
package thread;
class StopThread implements Runnable {
private boolean flag = true;
public void run() {
while (flag) {
System.out.println(Thread.currentThread().getName() + "....run");
}
}
public void changeFlag() {
flag = false;
}
}
class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 0;
while (true) {
if (num++ == 60) {
t1.interrupt();// 清除冻结状态
t2.interrupt();
st.changeFlag();// 改变循环标记
break;
}
System.out.println(Thread.currentThread().getName() + "......."
+ num);
}
System.out.println("over");
}
}
运行结果:部分截图
十四、什么时候写多线程?
当某些代码需要同时被执行时,就用单独的线程进行封装。
示例:
package thread;
class ThreadTest {
public static void main(String[] args) {
// 一条线程
new Thread() {
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().toString()
+ "....." + x);
}
}
}.start();
// 又是一条线程
Runnable r = new Runnable() {
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName()
+ "....." + x);
}
}
};
new Thread(r).start();
// 可以看作主线程
for (int x = 0; x < 1000; x++) {
System.out.println("Hello World!");
}
}
}
黑马程序员——Java基础---多线程
最新推荐文章于 2015-12-07 14:27:34 发布
本文详细介绍了Java中的多线程概念,包括进程与线程的关系、多线程的意义以及Java程序的运行原理。接着,通过实例展示了如何通过继承Thread类和实现Runnable接口创建线程,讲解了线程调度、线程控制方法,如线程休眠、线程礼让、后台线程等。此外,还讨论了线程安全问题,包括同步技术的使用和死锁的避免。最后,文章给出了多线程在实际场景中的应用例子,帮助读者理解何时应该使用多线程。
摘要由CSDN通过智能技术生成