java -- 线程(一)
线程与进程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
进程与线程的区别
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
- 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
- Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
- 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
线程的创建
继承Thread类
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义一个类继承Thread类
- 重写run 方法(线程任务)
- 开启线程
创建子类对象
调用thread类的start方法
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("sub... "+i);
}
}
}
public static void main(String[] args) {
// 创建自定义的线程对象
MyThread mt = new MyThread();
// 开启线程
mt.start();
// 一个线程仅可以被启动一次
// mt.start()
// 主线程执行
for (int i = 0; i < 50; i++) {
System.out.println("main... "+i);
}
}
线程名字的设置和获取
-
Thread类的方法
String getName()
可以获取到线程的名字。 -
Thread类的方法
setName(String name)
设置线程的名字。 -
通过Thread类的构造方法
Thread(String name)
也可以设置线程的名字。
public class Demo {
public static void main(String[] args) {
MyThread mt = new MyThread();
//设置线程名字
mt.setName("旺财");
mt.start();
}
}
class MyThread extends Thread{
public void run(){
System.out.println("线程名字:"+super.getName());
}
}
线程是有默认名字的,如果不设置,JVM会赋予线程默认名字Thread-0,Thread-1。
获取运行main方法线程的名字
Demo类不是Thread的子类,因此不能使用getName()方法获取。
Thread类定义了静态方法static Thread currentThread()
获取到当前正在执行的线程对象。
public static void main(String[] args){
Thread t = Thread.currentThread();
System.out.println(t.getName());
}
实现Runnable接口
java.lang.Runnable
接口类
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
public class Demo {
public static void main(String[] args) {
// 创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
// 创建线程对象
Thread t = new Thread(mr);
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("main " + i);
}
}
}
public class MyRunnable implements Runnable{
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。
匿名内部类方式创建线程
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
public static void main(String[] args) {
// 第一种方式 实际上需要的就是Thread类的子类对象
Thread t = new Thread(){
@Override
public void run() {
System.out.println("方式一 赋值!");
}
};
t.start();
new Thread() {
@Override
public void run() {
System.out.println("方式一 直接调用!");
}
}.start();
// 第二种方式 实际上需要的就是Runnable接口的实现类对象
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("方式二 赋值!");
}
};
new Thread(r).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("方式二 直接调用!");
}
}).start();
}
Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
Thread类API
睡眠sleep
public static void sleep(long time)
让当前线程进入到睡眠状态,到毫秒后自动醒来继续执行
public class Test{
public static void main(String[] args){
for(int i = 1;i<=5;i++){
Thread.sleep(1000);
System.out.println(i)
}
}
}
设置线程优先级
线程的切换是由线程调度控制的,我们无法通过代码来干涉,但是我们通过提高线程的优先级最大程度的改善线程获取时间片的几率
线程的优先级被划分为10级,值分别为1-10,其中1最低,10最高.Thread提供了3个常量来表示:
public static final int MIN_PRIORITY //1 最低优先级
public static final int NORM_PRIORITY //5 默认优先级
public static final int MAX_PRIORITY //10 最大优先级
// 更改线程的优先级
public final void setPriority(int newPriority)
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("t1: " + i);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("t2: " + i);
}
}
});
// 设置优先级
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
join
让当前线程等待,调用方法的线程进行插队先执行,执行完毕后,在让当前线程执行.对其他线程没有任何影响.
注意 此处的当前线程不是调用方法的线程 而是Thread.currentThread().
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("t1: " + i);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("t2: " + i);
}
}
});
t1.start();
// t1.join();
/* t1在此处插队时
此时 Thread.currentThread() 为 main 线程, 所以 main 线程等待, t1 线程插队
由于main 线程等待 t2线程还未开启, 因此t1执行完毕, main和t2抢
*/
t2.start();
t1.join();
/* t1在此处插队时
此时 Thread.currentThread() 依然为 main 线程, 所以 main 线程等待, t1 线程插队
而在这之前 t2 线程已经开启, 因此会出现两种情况:
1. t2 先执行完 t1 执行完之后 主线程执行
2. t1 先执行完 t2 和 主线程抢
*/
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
面试题
三个线程同时开启 保证线程一之后执行线程二 再之后执行线程三
class JoinMethodTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("t1: " + i);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 50; i++) {
System.out.println("t2: " + i);
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 50; i++) {
System.out.println("t3: " + i);
}
}
});
t1.start();
t2.start();
t3.start();
}
}
线程停止
public final void stop() 直接中断线程 此方法已过时 不安全
public boolean isInterrupted() 获取线程是否中断的状态 如果中断返回true 没中断返回false
public void interrupt() 中断线程 此时调用interrupted方法 会返回true
代码演示:
public class Test02 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <1000000 ; i++) {
System.out.println(i);
//是否有人要中断线程 如果有返回true 如果没有返回false
//让线程中断更为平滑 可以使用代码来控制中断
boolean b = Thread.currentThread().isInterrupted();
if(b){
break;
}
}
}
});
t1.start();
try {
Thread.sleep(2000);
// t1.stop(); //方法已经过时 不安全
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
用户线程与守护线程
java分为两种线程:用户线程和守护线程
/*
用户线程
我们正常写的代码都是用户线程
守护线程
用户线程存在 守护线程可以执行 用户线程执行完成 守护线程即使没有执行完 jvm也会退出
守护线程和用户线程没有本质的区别,唯一不同之处就在于虚拟机的退出:
如果用户线程已经全部退出运行了,只剩下守护线程存在,虚拟机会直接退出.但是只要有用户线程运行,虚拟机就不会退出.
设置守护线程:
public final void setDaemon(boolean on) on的值为true将线程设置为守护线程,需要在开启线程之前设置
*/
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1: " + i);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2: " + i);
}
}
});
/*
由于t2每次休眠50ms, 而t1休眠200ms, 所以t2先执行完
而t1并不是守护线程, 所以t2执行完后, t1继续执行
*/
// t1.start();
// t2.start();
//将t1设置为守护线程, t2执行完后, t1将不再执行, jvm直接退出
t1.setDaemon(true);
t1.start();
t2.start();
}
线程安全
多条线程操作同一资源时 可能出现数据错误 此时线程是不安全的
public class Demo {
public static void main(String[] args) {
SellTickets st = new SellTickets();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
Thread t3 = new Thread(st);
t1.start();
t2.start();
t3.start();
}
}
class SellTickets implements Runnable {
private int i = 20;
public void run() {
while (true) {
if (i > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖票 " + i--);
}
}
}
}
/*
Thread-2 卖票 19
Thread-0 卖票 20
Thread-1 卖票 18
Thread-1 卖票 17
Thread-2 卖票 15
Thread-0 卖票 16
Thread-2 卖票 14
Thread-0 卖票 13
Thread-1 卖票 12
Thread-1 卖票 10
Thread-0 卖票 9
Thread-2 卖票 11
Thread-2 卖票 8
Thread-0 卖票 8
Thread-1 卖票 6
Thread-0 卖票 5
Thread-2 卖票 4
Thread-1 卖票 3
Thread-2 卖票 2
Thread-1 卖票 1
Thread-0 卖票 0
Thread-2 卖票 -1
*/
发现程序出现了两个问题:
- 相同的票数
- 不存在的票,比如0票与-1票,是不存在的。
线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
根据案例简述:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。
也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
同步代码块
同步代码块:线程操作的共享数据进行同步。synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
// 格式
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
同步锁又称为对象监视器。同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
使用同步代码块解决代码:
public class Ticket implements Runnable{
private int ticket = 100;
private Object lock = new Object();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
synchronized (lock) {
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
}
}
注意:线程运行至同步代码块的时候,需要判断锁,获取锁,出去同步代码块后要释放锁,增加了很多操作,因此线程安全,程序的运行速度慢!
同步方法
同步方法:当一个方法中的所有代码,全部是线程操作的共享数据的时候,可以将整个方法进行同步。使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
// 格式
public synchronized void method(){
// 可能会产生线程安全问题的代码
}
使用同步方法代码如下:
public class Ticket implements Runnable{
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
sellTicket();
}
}
/*
* 锁对象 是 谁调用这个方法 就是谁
* 隐含 锁对象 就是 this
*
*/
public synchronized void sellTicket(){
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
同步锁是谁?
-
对于非static方法,同步锁就是this。
-
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加锁。public void unlock()
:释放锁。
使用如下:
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
lock.lock();
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
lock.unlock();
}
}
}
线程状态
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析。
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
等待和唤醒
Object类的方法
void wait()
// 在其他线程对用此对象的notify()方法或notifyAll方法前, 导致当前线程等待
void wait(long time)
// 当前线程等待 time 毫秒
void notify()
// 唤醒在此对象监视器上等待的单个线程
void notifyAll()
// 唤醒在此对象监视器上等待的所有线程
public class RelatedInObject {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (o) {
System.out.println("开始等待");
try {
o.wait();
// 等待3000毫秒后
// o.wait(3000);
System.out.println("已经被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("end");
}
}).start();
Thread.sleep(5000);
synchronized (o) {
System.out.println("开始唤醒");
o.notify();
}
}
}