多线程
个人理解:要想理解多线程,就得先理解线程,要想明白线程,那么就要掌握进程啦,下面我们从进程开始,来一步步学习多线程吧~~
进程
是一个正在执行的程序。
每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
线程
就是进程中的一个独立的控制单元。线程在控制着进程的执行。只要进程中有一个线程在执行,进程就不会结束。
一个进程中至少有一个线程。
多线程
在java虚拟机启动的时候会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行。
而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,
还有负责垃圾回收机制的线程。像这种在一个进程中有多个线程执行的方式,就叫做多线程。
举例:迅雷下载,360管理界面。
我们如何实现多线程程序呢?
由于线程是依赖于进程存在,而进程是由操作系统创建的,并且java语言是不能直接调用操作系统的功能。
所以,为了方便对多线程程序的时候,java就提供了线程的API对应的类-----线程类:Thread。
通过查看API,我们知道创建线程的方式有2种。
方式1:继承Thread类
A:定义一个类继承Thread类。
B:子类要重写Thread类的run()方法。
目的:将线程要执行的代码储存在run方法中,让程序运行。
C:让线程启动并执行。
1. 创建类的实例对象,就相当于创建了一个线程。
2 调用线程的start方法,该方法有两个作用
a. 启动线程
b. 调用run方法
start:调用到底层让控制单元去执行的动作。
run:封装线程要运行的代码
注意:调用线程的start方法是开启线程并执行该线程的run方法。
而直接调用run方法仅仅是对象调用方法,而线程创建了,并没有运行
下面写一个小例子帮助理解:
package cn.itcast01;
/*
* 为了看到每次确实在变化,我们要是能够知道线程对象的名字有多好呢?
* 在Thread类中提供了一个方法:
* public final String getName():获取线程对象的名称。默认情况下,名字的组成 Thread-编号(编号从0开始)
* public final void setName(String name):设置线程名称。
*/
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"-----Hello "+i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread mt1=new MyThread();
MyThread mt2=new MyThread();
mt1.setName("QQ聊天");
mt2.setName("微信");
mt1.start();
mt2.start();
}
}
运行后结果:
不知道有没有人和我一样问过:为什么要覆盖run方法呢?
Thread类用于描述线程,Thread类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。
也就是说Thread类中的run方法,用于存储线程要运行的代码。
方式2:
A:创建一个类实现Runnable接口
B:重写run()方法
C:创建类的实例
D:把类的实现作为Thread的构造参数传递,创建Thread对象
这时候肯定会有人问:既然有了继承Thread类的方式,为什么还要有实现Runnable接口的方式?
A:避免单继承的局限性,如果你继承了Thread类,那么你就不能再继承其他的类,这样就会有好多局限性。
B:实现接口的方式,只创建了一个资源对象,更好的实现了数据和操作的分离。
一般我们选择第二种方式。
下面用第二种方式实现:
package cn.itcast01;
/*
* public static Thread <strong>currentThread():</strong>返回当前正在执行的线程对象引用
*/
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i <100; i++) {
System.out.println(Thread.currentThread().getName()+"-----Hello "+i);
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
MyRunnable mr=new MyRunnable();
Thread t1=new Thread(mr);
Thread t2=new Thread(mr);
t1.setName("QQ聊天");
t2.setName("微信");
t1.start();
t2.start();
}
}
注意:启动线程并执行,是不能使用run()方法的。这个时候,必须使用另外的一个方法。
这个方法名是start()。这个方法其实做了两件事情。
第一,让线程启动。第二,自动调用run()方法。
学到这儿,肯定会有人问学习多线程就这些好处啊?下面我们来总结一下多线程的意义:
多线程的出现能让程序产生同时运行效果。可以提高程序执行效率。
例如:在java.exe进程执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象,而同时对象调用完后,就成了垃圾。
如果垃圾过多就有可能是堆内存出现内存不足的现象,只是如果只有一个线程工作的话,程序的执行将会很低效。
而如果有另一个线程帮助处理的话,如垃圾回收机制线程来帮助回收垃圾的话,程序的运行将变得更有效率。
线程的生命周期(几种状态):
被创建:等待被start方法被调用启动。
运行状态:具有执行资格和执行权。
临时状态(阻塞状态):具备执行资格,但是没有执行权。
冻结状态:有两个状态
a. 睡眠:sleep(time)方法
b. 等待:wait()方法。放弃了执行资格。
当sleep方法时间到或者调用到notify()方法时,获得执行资格,变为临时状态。
变为临时状态后,如果CPU空闲再执行。
消亡状态:run()方法结束,或者遇到stop()方法。
在这儿需要注意的是: 总结:没有执行资格是冻结状态,有执行资格没有执行权是阻塞状态,执行权执行资格都有是运行状态。
线程的状态图如下:
线程的安全问题:
谈到线程安全问题,我想先模拟一下火车站窗口卖票的场景,来让大家看一下出现的问题:
public class TicketRunnable implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
//t1,t2,t3,t4过来了
//tickets = 1
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
}
}
public class RunnableTest {
public static void main(String[] args) {
TicketRunnable tr=new TicketRunnable();
Thread t1=new Thread(tr);
Thread t2=new Thread(tr);
Thread t3=new Thread(tr);
Thread t4=new Thread(tr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t4.setName("窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行后结果:
我们发现竟然票数还有负的,目前这个代码符合真实的买票程序,但是有问题,居然出现了负数票的情况。
那么,产生的原因是什么呢?
线程的随机性和延迟性,导致了线程访问共享数据出现了问题。
怎么解决呢?
在多线程程序中,一般来说,不会是所有的代码都有问题。
所以,我们只需要找到那些可能出现问题的代码,然后把它们包起来,看做一个整体;
只有这个整体完毕,别人才能继续访问,为了安全,把这个整体包起来的代码加个锁,给锁起来。
那么,怎么找呢?(多线程出问题的判断条件)
A:看有没有共享数据
B:看对共享数据的操作是不是多条语句
C:看是不是在多线程程序中
找到后,就把同时满足着三个条件的代码给锁起来。
怎么锁嘞?
java提供了一种锁机制方式:
synchronized(锁对象)
{
需要被锁的代码;
}
锁对象:怎么做呢? 反正不知道,所以,我们就用Object类的实例。
注意:多个线程必须使用同一把锁。
把上面那个代码改进后:
public class TicketRunnable implements Runnable {
private int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
//tickets=1
//t1,t2,t3,t4都来了
//假设t1抢到,看到是开的状态
synchronized (obj) {
//锁对象的状态:开,关
//t1进来了,给外界了一个关的状态。
if (tickets > 0) {
try {
//t1睡了
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
} //t1把状态修改为开
}
}
同步代码块的锁对象是什么?
同步代码块的锁是任意对象。
同步方法:就是把锁加在方法上
格式:在方法上加synchronized关键字
同步方法的锁对象是谁? this对象
静态方法的锁对象是谁呢?
是当前类的字节码文件对象。
类名.class - Class类型的对象
以后我们用同步代码块还是同步方法呢?
被同步的内容越少越好,所以,一般使用同步代码块。
如果一个方法内部全部都被同步了,那么,可以考虑使用同步方法。
public class TicketRunnable implements Runnable {
private static int tickets = 100;
private Object obj = new Object();
private Demo d = new Demo();
@Override
public void run() {
int x = 0;
while (true) {
if (x % 2 == 0) {
// synchronized (this) {
synchronized (TicketRunnable.class) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
} else {
check();
}
x++;
}
}
private static synchronized void check() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
}
线程代码的死锁问题
死锁问题一般是在当同步中嵌套同步时,就有可能出现死锁现象。
最经典就是哲学家就餐问题了。
下面是一个死锁的例子:
package cn.itcast_07;
/*
* 死锁问题:
* 5个哲学家的故事。
* 5个哲学家去吃饭,菜饭都上齐了,筷子也上了,但是,一人只有一只筷子,每个人,先思考一会,把筷子借给别人,
* 然后,别人吃完了,自己在吃。假如这5个人都饿了,他们就会拿起自己的筷子,但是只有一只,都等待这个别人
* 放下那一只筷子,然后好拿过来吃饭,而没有任何一个人愿意先放下筷子,所以,就出现了死锁。
*/
public class DieLock extends Thread {
// 定义一个布尔型的标记
private boolean flag;
//建立对象时接收boolean类型的数据
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
// 建立外锁A
synchronized (MyLock.objA) {
System.out.println("true -- objA");
// 建立内锁 <strong>B</strong>
synchronized (MyLock.objB) {
System.out.println("true -- objB");
}
}
} else {
// 建立外锁B
synchronized (MyLock.objB) {
System.out.println("false -- objA");
// 建立内锁A
synchronized (MyLock.objA) {
System.out.println("false -- objB");
}
}
}
}
}
//创建一个类用于放锁
class MyLock {
public static final Object objA = new Object();
public static final Object objB = new Object();
}
public class DieLockDemo {
public static void main(String[] args) {
DieLock d1 = new DieLock(true);
DieLock d2 = new DieLock(false);
//开启线程
d1.start();
d2.start();
}
}
运行后结果为:
从上面的结果可以看到,两个人一人拿了一支筷子,想要对方的筷子,然后先吃饭,最后两人都不放,从而产生了死锁。
线程间的通信
线程间的通信问题,即不同种类的线程间对共享数据的操作问题;
其实就是多个线程在操作同一个资源,但是操作的动作不同。
举例:以学生作为资源举例。
学生是资源,我们就可以对学生的属性进行赋值,也可以获取学生的属性值使用。
那么,我们要写哪些内容呢?
学生类
设置学生属性的类
获取学生属性的类
测试类
/*
* 线程间的通信问题。刚开始的代码出现的问题都有:
* 现在问题就产生了:
* 出现了:
* 张三 20
* 李四 23
* 李四 20
* 张三 23
*
* 我们先分析问题什么有这个问题,然后再解决问题。
* 由于线程的随机性产生的问题。
*
* 然后我们回到上面的那个总结:
* A:是否有共享数据
* B:是否有多条语句操作共享数据
* C:是否在多线程环境中
*
* 出问题的原因我们知道了,那么怎么解决呢?
* 用同步解决。
* 我们把setStudent给加同步了,但是,还是有问题。原因是需要对多个线程都要加同步。
* 我给两个操作都加同步了,还是出问题,这一次的原因是:两种操作的锁对象不一致。
* 当我们把所有的操作都加同步,并且锁用同一个以后,我们的数据就没有问题了。
*
* 这个时候,我们看到了一个不好的现象,就是同一个数据一大片一大片的输出,原因就是每一次获取到CPU的执行权,就足够输出很多次数据。
* 我想,能不能输出一个张三,然后输出一个李四,然后再输出一个张三,再输出一个李四...
*
* 如何能够做到这个效果呢?
*
* 做这个效果前,我们先分析一个问题,问题是:我们的程序其实是有一点点逻辑问题的。
* 如果没有设置数据,就直接获取数据了。这应该不符合逻辑。
* 正常逻辑是:
* 针对输出:
* 判断是否有数据,如果有就输出。否则,就等待设置数据。
* 针对设置:
* 判断是否有数据,如果有就等待输出数据,否则,就输出。
* 等待唤醒机制:
*
* Object类中:
* wait() 让线程处于等待状态
* notify() 唤醒等待的线程
*/
class Student { //定义一个学生类
String name;
int age;
// flag作为数据标识存在,true表示有数据,false表示没有数据。
boolean flag = false;
}
//设置学生属性的类
class SetStudent implements Runnable {
private Student s;
public SetStudent(Student s) {
this.s = s;
}
@Override
public void run() {
// t1过来了
int x = 0;
while (true) {
synchronized (s) {
//如果有数据,就等待。
if(s.flag){
try {
s.wait();//t1等待了,
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (x % 2 == 0) {
s.name = "李四"; // t2抢到;
s.age = 26;
} else {
s.name = "张三";// t2抢到;
s.age = 29;
}
//修改标记
s.flag = true;
s.notify();//唤醒了t2,这个时候,可能是t1继续,或者t2抢到。
}
x++;// x=1,t2抢到;x=2,t2抢到;
}
}
}
// 获取学生属性的类
class GetStudent implements Runnable {
private Student s;
public GetStudent(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if(!s.flag){
try {
s.wait();//t2等待了。wait()的线程,被唤醒后,继续执行。wait()方法出现后,对应的线程就释放了锁对象
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.name + "***" + s.age);
//修改标记
s.flag = false;
s.notify(); //t1被唤醒,但是不一定会立马执行。
}
}
}
}
//测试类
public class StudentTest {
public static void main(String[] args) {
Student s = new Student();// 资源类
SetStudent ss = new SetStudent(s);
GetStudent gs = new GetStudent(s);
Thread t1 = new Thread(ss);
Thread t2 = new Thread(gs);
t1.start();
t2.start();
}
}
线程的几个常用方法:
1)优先级
通过查API,我们知道 线程默认优先级是5。范围是1-10。
public final int getPriority() : 获取线程优先级
public final void setPriority(int newPriority) : 更改线程的优先级。
MAX_PRIORITY 最高优先级10
MIN_PRIORITY 最低优先级1
NORM_PRIORITY 分配给线程的默认优先级5
在这儿需要注意的是:优先级可以在一定的程度上,让线程获较多的执行机会。
2)暂停线程
public static void yield() : 暂停当前正在执行的线程对象,并执行其他线程。
是为了让线程更和谐一些的运行,但是你不要依赖这个方法保证,如果要真正的实现数据依次输出,请使用等待唤醒机制。
3)加入线程
public final void join():等待该线程终止。
一旦有join()线程,那么,其他的线程必须都等待,一直到该线程结束。
4)守护线程
public final void setDaemon(boolean on):设置线程为守护线程,一旦前台(主线程),结束,守护线程就结束了。
在这要知道的是:main方法本身就是一个线程。
最后总结一些面试中经常遇到的关于线程方面的题:
1. wait(); notify() ; notifyAll() 都使用在同步中,因为要对持有监视器(锁)的线程操作。
为什么这些操作线程的方法要定义在Object类中呢?
因为这些方法在操作同步线程时,都必须要标识他们所操作的线程持有的锁。
只有同一个锁上的被等待线程,可以被同一个锁notify()唤醒,不可以对不同锁中的线程进行等待唤醒。
也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object中。
2. wait(),sleep(time)有什么区别?
wait():释放CPU执行权,释放锁。
sleep():释放CPU执行权,不释放锁。
3. 为什么要定义notifyAll()?
因为在需要唤醒对方线程时,如果只使用notify(),会容易出现只唤醒本方线程的情况,导致程序所有的线程都等待。
notify()只通知队列中的第一个线程,而notifAll()通知的是等待队列中的所有线程
JDK1.5中提供了多线程的升级解决方案
将同步synchronized替换成显示的Lock操作,将Object中的wait() ; notify() ; notifyAll()替换成condition对象,该对象可以通过Lock锁进行获取。
到此,线程的知识点儿就总结到这儿吧,有些理解的不对的地方,真诚地希望大家帮我指正出来,谢谢!!!