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

黑马程序员——Java基础---多线程

2015-05-04 00:23 537 查看
——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-

一.进程概述

进程就是正在执行的一个程序。可以说是静态的,当进程一旦执行,那么就会在内存中给其分配一定的内存空间,供其运行。

从系统上来理解:进程有单进程和多进程

单进程:就是只能运行一个程序,只有这个程序运行完了,才能运行其他的进程,例如:DOS系统,他的特点就是只要是一种病毒,就会死机,原因就是采用的是单进程

.多进程:就是可以同时运行多个程序,例如现在的Window系统,采用的就是多进程

Java虚拟机把其封装成了对象,供其使用。

进程和线程的之间的举例说明:打开qq程序,qq就是一个进程,那么线程就是其中的qq连天窗口。

二.线程的概述

1.概述:线程是动态的,是进程中的控制单元,一个进程中可以执行多个线程,线程控制着进程。

2.一个进程中至少存在一个线程在执行,

例如:java虚拟机启动后,不只是存在的一个线程,一个是主线程,一个是垃圾回收的线程。

3.线程的执行必须是在有进程的基础之上,当一个线程消失了,那么进程不一定会消失,但是进程消失了,那么线程肯定会消失。

4.主线程:在系统中就是进程一旦创建,就是创建一个线程,那么就是个主线程,那么再有其他的线程生成的话,那就是其子类,在java虚拟机中的主线程是在运行主函数的线程。

5.多线程:多线程会提高执行速度(这只是多线程其中的一个功能例如:下载)当是多线程的话,那么各个线程之间会有优先级。当线程抢到了CPU,谁就会执行。

三.线程的创建方法

1.继承Thread类。

(1)定义类继承Thread类。

(2)复写run()方法。

(3)启动线程,使用start()方法

2.实现接口Runnable接口。

(1)定义类(A)实现(implements)接口Runnable。

(2)重写run()方法。

(3)创建线程与类(A)创建联系,开启线程

总结:实现方式和继承方式的区别

1、实现方式避免了单继承的局限性,在定义线程时,建议使用实现方法。

2、继承方式:线程代码存放在Thread子类run方法中;

实现方式:线程代码存放在Runnable接口的子类run方法中。

四.线程的创建—Thread类

1.代码举例:

public class MyThread extends Thread {
public void run(){
for(int i=0;i<10;i++)
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
结果:
main--0
Thread-0--0
main--1
Thread-0--1
main--2
Thread-0--2
Thread-0--3
main--3
Thread-0--4
main--4
Thread-0--5
main--5
Thread-0--6
main--6
Thread-0--7
main--7
Thread-0--8
main--8
Thread-0--9
main--9


从结果上可以看出,每次的运行结果都不一样,原因就是自定义的那个线程和主线程抢夺CPU,那一个先抢到CPU,那个就先执行。

2.start()和run()的区别和联系

联系:线程调用start()方法实际上是调用的run()方法,run()方法中存储的线程运行的代码

区别:调用start()方法是启动线程,直接调用run()方法的话,那么就和调用普通的方法是一样的,没有启动线程。

public class MyThread extends Thread {
public void run(){
for(int i=0;i<10;i++)
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
public class Text {
public static void main(String[] agrs) {
MyThread tt = new MyThread();
tt.run();
for (int i = 0; i < 10; i++)
System.out.println(Thread.currentThread().getName() + "--" + i);

}
}


3.复写run()

run() 中存在的是线程运行的代码,对于主线程中运行的代码存储在main()方法中。

4.疑问解答

在主线程中,运行时候,也是按照顺序的。直到新的线程开启了,那么才会和存在线程抢夺CPU。

public class MyThread extends Thread {
public void run(){
for(int i=0;i<10;i++)
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
public class Text {
/*
* 主线程开始从上向下执行,直到执行到了tt线程,启动了tt线程,
* 但是此时主线程已经运行结束了,所以运行结果都是一样的。
* */
public static void main(String[] agrs) {
for (int i = 0; i < 10; i++)
System.out.println(Thread.currentThread().getName() + "--" + i);
MyThread tt = new MyThread();
tt.start();
}
}
public class Text1{
/*
* 主线程开始从上向下执行,开始就创建了线程,并且启动了tt线程,
* 然后tt线程会和主线程抢夺CPU,谁抢到就会执行谁,所以此时运行结果是不确定的
* */
public static void main(String[] agrs) {
MyThread tt = new MyThread();
tt.start();
for (int i = 0; i < 10; i++)
System.out.println(Thread.currentThread().getName() + "--" + i);

}
}


五.线程状态

线程状态如下图所示:





六.线程名称

1.默认的线程

默认线程的名称是Thread-0,Thread-2…….。主线程的名称是:main

public class Text {
public static void main(String[] agrs) {
MyThread tt = new MyThread();
System.out.println("默认的线程名称:"+tt.getName());
//tt.start();
System.out.println( "主线程的默认的名字:"+Thread.currentThread().getName());

}
}


2.自定义线程

自定义线程名称和获得线程名称名

设置线程名称:可以用setName()方法,可以用构造方法设置线程名称

获得线程名称:用getName()方法获得线程名称

Thread.currentThread()获得当前运行的线程对象

public class MyThread extends Thread {
public MyThread (String name){//通过构造方法设置线程名称
super(name);
}
public void run(){

}
}
public class Text {
public static void main(String[] agrs) {
MyThread tt = new MyThread("线程A");
System.out.println("线程名称:" + tt.getName());
// tt.start();
/*
* 也可以通过setName()来设置线程名称
*/
tt.setName("setName");
System.out.println("线程名称:" + tt.getName());
}
}
结果:
线程名称:线程A
线程名称:setName


七.售票小例子

public class Tick extends Thread {
private int tick = 10;

public void run() {
while (tick > 0) {
System.out.println(Thread.currentThread().getName() + "卖了第"
+ (tick--) + "张票");
}
}
}
public class Text {
public static void main(String[] agrs) {
new Tick().start();
new Tick().start();//这样相当于开了3个线程,就是3个窗口
new Tick().start();//但是这样相当于各个窗口买各自的票,而不是共享票

}
}


这样的结果相当于买了40张票,但是本来就10张票,重复卖了

结果:

Thread-0卖了第10张票

Thread-2卖了第10张票

Thread-1卖了第10张票

Thread-1卖了第9张票

Thread-2卖了第9张票

Thread-0卖了第9张票

解决办法:

1. 把票(tick)定义成静态的,但是如果定义静态的话,但是静态变量声明周期长,不建议这样使用

public class Tick extends Thread {
private static int tick = 10;

public void run() {
while (tick > 0) {
System.out.println(Thread.currentThread().getName() + "卖了第"
+ (tick--) + "张票");
}
}
}


2.可以把值开启一个线程,结果没有错,但是和日常生活多个窗口共同卖票不符合

public class Text {
public static void main(String[] agrs) {
new Tick().start();
}
}


3.使用实现Runnable接口线程处理此情况

步骤

(1).创建类(A)实现接口Runnable,复写run()方法

(2).创建Thread对象,然后把类A的对象作为参数传给Thread的构造函数,这是把其关联起来。

(3).通过创建的线程对象,然后开启线程。

解答:复写run()方法,里面存的是线程要运行的代码

因为Runnble接口中只有一个run()方法,所以没有办法启动线程,所以还得使用Thread类来启动线程,所以要把Runnable的子类的对象作为参数传给Thread类构造函数。

售票例子(Runnable接口)

public class Tick implements Runnable {
private int tick = 10;

public void run(){
while (tick > 0) {
System.out.println(Thread.currentThread().getName() + "卖了第"
+ (tick--) + "张票");
}
}
}

结果:
Thread-0卖了第10张票
Thread-1卖了第10张票
Thread-0卖了第9张票
Thread-1卖了第8张票
Thread-0卖了第7张票
Thread-1卖了第6张票
Thread-0卖了第5张票
Thread-0卖了第4张票
Thread-0卖了第3张票
Thread-0卖了第2张票
Thread-0卖了第1张票
Thread-1卖了第0张票


从结果上可以看出,这是我们想要的那种结果,多个窗口共同买共有的票。

总结:Runnable接口和Thread类的区别与联系

(1).Runnable接口可以避免单继承的限制,要是继承Thread类的话,那么就不能继承其他的类了,因为只能单继承,如果实现了接口Runnale后,还可以继承其他的类,或是是实现其他的接口

(2).Runnable实现了资源的共享,例如(售票程序的票)

(3).Runnable实增强了程序的健壮性,代码能够被多个程序共享,实现了数据 与代码是独立的。

(4).实现Runnable接口的线程的运行代码存在实现Runnable接口子类的run()方法中,继承Thread类的线程的代码存在Thread子类的run()方法中。

所以在以后的开发中,使用Runnble接口比较好,更多。

八.线程的安全问题

1.原因

当多条语句执行多个线程共享的资源,执行到一部分后,执行权被抢夺了,导致共享资源的不正正常修改,所以就产生了线程的安全问题。

例如:售票小例子

public class Ticket implements Runnable {
private int tick = 10;

public void run() {
while (true) {
try {
if (tick > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+ "卖了第"
+ (tick) + "张票");
tick--;
}
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}
}
public class Text {
public static void main(String[] agrs) {
Ticket tt=new Ticket();
new Thread(tt).start();
new Thread(tt).start();
}
}
结果:
Thread-1卖了第10张票
Thread-0卖了第10张票
Thread-1卖了第8张票
Thread-0卖了第8张票
Thread-0卖了第6张票
Thread-1卖了第5张票
Thread-1卖了第4张票
Thread-0卖了第3张票
Thread-0卖了第2张票
Thread-1卖了第2张票
Thread-0卖了第0张票


结果出现了:产生了重复的票,并且出现了0票,那么这是不正常的现象的。

2.同步代码块—解决办法

(1)利用的是关键子(synchronized)来处理线程同步问题,保证线程安全,锁就好比门上的锁一下,执行的顺序是:首先是判断锁是否是开着的,若是开着的,那么就可以进去(执行同步代码)然后把锁锁上,执行完后,把锁释放(把锁开开),判断锁是锁着的,那么就在外面等着,直到里面的人把锁开开,出来。(这样保证的是同步代码在某一段时间只有一个线程在执行。)

(2).格式

synchronized(对象){执行共享资源的代码}

(3).同步规则

必须是多个线程执行(至少两个)才可以产生同步,必须是多个线程使用的是同一个锁,

优点:解决了多线程的安全问题

弊端:多线程需要判断,那么就会消耗时间,消耗资源

public class Ticket implements Runnable {
private int tick = 10;

public void run() {
while (true) {
synchronized (this) {
try {
if (tick > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()
+ "卖了第" + (tick) + "张票");
tick--;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
}
}
结果:
Thread-0卖了第10张票
Thread-0卖了第9张票
Thread-0卖了第8张票
Thread-1卖了第7张票
Thread-1卖了第6张票
Thread-1卖了第5张票
Thread-1卖了第4张票
Thread-1卖了第3张票
Thread-1卖了第2张票
Thread-1卖了第1张票


这样结果就和我们正常想要的是一样的,没有异常现象

3.同步方法—解决办法

(1)概述

因为方法和代码块都是用来封装代码的,那么代码块可以使用锁来解决同步问题,那么函数也可以使用锁,使方法也可以操作同步问题。

(2)格式

public synchronized 方法返回类型 方法名(参数){方法体}

(3)使用

怎眼确定锁的位置,寿面判断那些是线程运行的代码,那些是共享资源,那些同步代码是执行的是同步资源,那么就把那些操作同步资源的语句使用锁来进行括起来。

例如:客户向银行存钱,每次存三次,每次存100,分两个地方存户存

思路:共享资源:总价钱,银行,有两个线程(模拟两个地方存)

**
* 银行类
*/
public class Bank {
private int sum;// 表示的是当前账户的总额

public synchronized void add(int mon) {// 存方法,加锁的话,那么里面的语句在一段时间内必须是一个线程在执行
sum += mon;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sum=" + sum);
}
}
public class Cus implements Runnable {
private Bank bank=new Bank();
public void run(){//线程执行的代码(线程)
for(int i=0;i<3;i++){
bank.add(100);
}
}
}
public class Text {
public static void main(String[] agrs) {
Cus tt=new Cus();//客户
new Thread(tt).start();//开始存
new Thread(tt).start();//开始存

}
}
结果:
sum=100
sum=200
sum=300
sum=400
sum=500
sum=600


(4)同步代码块和同步函数的选择

假如连续的代码是同步代码,并且都是执行的都是共享资源,那么就可以把其抽取成函数,并且定义为同步函数

假如一个函数中是全是同步代码,并且执行的全是共享资源,那么就把其定义为同步函数

(5)扩展

回顾:一共分为四种代码块

普通代码块:就是定义在方法中的代码块。

构造块:定义在类中的代码块,优先于构造方法,重复调用。

静态块:使用static关键字声明的,只执行一次,优先于构造块。

同步代码块:使用synchronized关键字声明的代码块,称为同步代码块。格式:synchronized(同步对象){}

4.锁对象的确定

(1)同步函数的锁是this

验证思路:还是使用的卖票程序,使用两个线程,他们执行的代码不同,一个是同步代码块(锁的对象是this synchronized(this){}),另一个线程执行的同步函数,如果结果没有异常,那么他们就完全符合线程同步的规则,

多个线程执行的是同步代码,并且他们的是锁是同一个锁,因为同步代码块的锁对象是this,那么此时同步函数锁的对象也是this

/*售票机*/
public class Ticket implements Runnable {
private int tick = 100;
public boolean flag = true;

public void run() {
if (flag) {
while (true) {
synchronized (this) {
try {
if (tick > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()
+ "卖了第" + (tick--) + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

} else
while (true)
show();
}

public synchronized void show() {// 同步函数
try {
if (tick > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+ "卖了第"
+ (tick) + "张票");
tick--;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Text {
public static void main(String[] agrs) {
Ticket tt=new Ticket();
new Thread(tt).start();
/*这里是主线程睡眠一下,让线程1先运行,防止直接改变标志位*/
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
tt.flag=false;//更改标志位
new Thread(tt).start();//开始存

}
}

部分结果:
Thread-0卖了第100张票
Thread-0卖了第99张票
Thread-0卖了第98张票
Thread-0卖了第97张票
Thread-0卖了第96张票
Thread-0卖了第95张票
Thread-1卖了第94张票
Thread-1卖了第93张票
Thread-1卖了第92张票


从结果上额可以看出,结果是正常的,所以结论是正确的,同步函数的锁对象是this

(2)静态同步函数锁对象

静态同步函数的锁是:该方法所在类的字节码文件对象:类名.class

验证方法:和上面的一样,把同步代码块的锁对象改为:Ticket.class

/*售票机*/
public class Ticket implements Runnable {
private static int tick = 100;
public boolean flag = true;

public void run() {
if (flag) {
while (true) {
synchronized (Ticket.class) {
try {
if (tick > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()
+ "卖了第" + (tick--) + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

} else
while (true)
show();
}

public static synchronized void show() {// 同步函数
try {
if (tick > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+ "卖了第"
+ (tick) + "张票");
tick--;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


结果:和上面的一样,正常。

我们可以这样思考,静态方法是随着类的加载而加载,此时没有类的对象,但是还是要找个对象,那么就是类的字节码文件的对象。

九.死锁

1.死锁产生的原因

死锁产生的原因是:一个所中还有另外一个锁,但是这两个锁对象是不相同的,其中一个锁(A)需要另外一个锁(B),而锁(B)也需要锁(A),这样有时候她们都不会妥协,那么就会产生死锁。

2.举例死锁

class MyLock {
static Object lockA = new Object();
static Object lockB = new Object();
}

public class DeadLock implements Runnable {
public boolean flag = true;

public DeadLock(boolean f) {
this.flag = f;
}

public void run() {
if (flag) {
// while (true) {
synchronized (MyLock.lockA) {
System.out.println("ifLockA");
synchronized (MyLock.lockB) {
System.out.println("ifLockB");
}
// }
}
} else {
// while (true) {
synchronized (MyLock.lockB) {
System.out.println("elseLockB");
synchronized (MyLock.lockA) {
System.out.println("elseLockA");
}
// }
}
}
}
}
public class Text {
public static void main(String[] agrs) {
DeadLock l1 = new DeadLock(true);// 第一个线程
DeadLock l2 = new DeadLock(false);// 第二个线程
new Thread(l1).start();// 开启线程
new Thread(l2).start();// 开启线程
}
}

结果:可能会死锁
例如:
if LockA
else LockB


这就产生了死锁

(1). 资源共享的时候需要进行同步操作

(2). 程序中过多了同步操作会产生死锁

死锁就是程序中互相的等待。

明白了死锁的原理以后在编写程序中就要尽量避免死锁。

十、停止线程及线程的其它方法

如何停止线程?

1、 定义循环结束标记,只有让run方法结束,开启多线程运行,运行代码通常是循环结构。只要控制住循环就可以让run方法结束,也就是线程结束。

特殊情况:

当线程处于冻结状态,就不会读到标记,线程也就不会结束。当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。Thread类中提供来interrupt方法来强制清除冻结状态。

2、 使用interrupt(中断)方法该方法是结束线程的冻结状态,是线程回到运行状态中来。

线程的其它方法

1、守护线程

setDaemon(): 当剩下的线程都为守护线程是时,java虚拟机退出;代码运行结束。

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

2、Join方法

当A线程执行到B线程的.join()方法时,A就等待,等B线程都执行完,A才会执行。Join可以临时加入线程执行。Join方法会抛出异常。

结语:单单掌握这些线程的基本知识是不够的,实际用到的线程知识却很灵活,本以为自己掌握的特别好,今天编写关于线程的程序时却感觉力不从心,无从下手,所以一定要多理解,勤动脑思考。

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