线程概念
1.进程与线程
进程:正在执行的程序为一个进程。进程负责了内存的划分。
windows号称多任务的操作系统,那么windows是同时运行多个程序么?
从宏观角度:windows同时执行多个任务。
从微观角度: cpu执行多个进程时,做了快速切换动作,即按照一定的算法切换执行进程。用户察觉不到。与其说是进程做资源抢夺动作,不如说是线程在做资源抢夺。
线程:线程在一个进程中负责代码的执行,即进程中的一个执行路径。
多线程:在一个进程中,有多个线程执行不同的任务。
2.多线程的好处与弊端
多线程的好处:
- 解决了一个进程可以执行多个任务。
- 提高了资源利用率
多线程的弊端:
- 降低一个进程中线程的执行概率
- 增加了cpu的负担
- 会引发线程安全问题
线程的创建
方式一:继承Thread类,重写run方法。
方式二:实现Runnable接口,重写run方法。然后将对象传给Thread对象执行。
推荐使用第二种,java只支持单继承,允许多继承。
注意事项:
1.Runnable的实现类是线程对象么?
===》只有Thread或其子类才是线程对象。
2.为什么要把Runnable实现类的对象作为实参传给Thread对象呢?作用是什么》
===》Thread类把Runnable实现类中的run方法作为线程的任务代码来执行。
this和currentThread在下面代码中不是同一个对象。this在代码中指的的Runnale实现类的当前对象。Thread,currentThread()指的是线程对象。
线程的生命周期
1。创建(NEW)
创建线程对象后,该对象处于线程的新建状态,JVM只是为其对象分配了对象。没有线程的任何动态性能。
2。可运行状态(Runnable)
线程对象调用start()方法后,线程就会进入可运行状态。此时获得了CPU的等待资格,但没有获取CPU的执行权,线程能否执行还需要CPU进行资源调度。
3.运行状态(Running)
线程获取CPU的执行权,此时进入运行状态。由于cpu不断做出切换动作,所以,此时线程会在运行状态与可运行状态之间来回切换。
4.阻塞状态(Blocked)
一个正在执行的线程在做耗时的IO流操作、wait()、sleep()等阻塞方法时,会进入阻塞状态(Blocked)
-----线程在获取一个对象的同步锁时,如果该锁没有被别的线程释放,则当前线程进入阻塞状态。只有当同步锁被释放后,该线程才会重新进入可运行状态。
-----线程在执行耗时的IO流操作时,会进入阻塞状态。当IO流结果有返回,才会重新进入可运行状态。
-----线程执行Thread.sleep(1000)方法时,会进入阻塞状态。只有当休眠时间结束,才会进入可运行状态。
-----当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,notify()方法唤醒。
——一个线程调用了另一个线程的join()方法时,当前线程进入阻塞状态。等新加入的线程运行结束后会结束阻塞状态,进入就绪状态。
线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,即结束阻塞的线程需要重新进入可运行池中,等待系统的调度。
5.死亡状态(Terminated)
线程的run()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能转换为其他状态。
创建(new) —》可运行状态(Runnable)–》(《–》阻塞状态–》 )—》 运行状态 (Running) --》死亡状态(Terminated)
线程常用方法
Thread(String name) 重写Thread的带参构造方法,指定线程名称。
t.setName(String name) 设置线程名称
t.setPriority(int i) 设置线程的优先级,设置数字越大,优先级越高 【1-10】
t.getPriority() 获取线程的优先级。(线程的优先级默认为5)
Thread.sleep(1000) 让线程休眠进入阻塞状态。这个方法由哪一个线程来执行,哪一个线程就休眠。不会释放锁资源
Thread.currentThread() 这个方法由哪一个线程来执行,就返回哪一个线程对象。
public static void main(String[] args) throws IOException, InterruptedException, ParseException {
App2 a=new App2("看片线程");
a.start();
System.out.println("看片线程优先级:"+a.getPriority());
System.out.println("主线程优先级:"+Thread.currentThread().getPriority());
}
线程安全问题
例子:
三个窗口同时卖票,会出现票据卖出重复问题。
出现线程安全问题根本的原因:
1.存在多个线程,并且线程之间共享资源。
2.有多个语句操作共享资源
线程安全问题解决方案
sun公司提供线程同步机制解决线程安全问题。
同步机制:
- 同步代码块
- 同步方法
同步代码块注意事项:
1.任意一个对象都可以作为锁对象。因为所有对象内部都维护了一个状态位,java同步机制就是使用了对象中的状态位作为锁标识。
2.在同步代码块中调用Thread.sleep()方法,并不会释放锁资源。【上卫生间关门】
3.同步机制影响执行效率,所以只有在真正线程安全问题下才使用。
4.多线程操作的锁对象必须是唯一共享的。
同步方法注意事项:
静态的同步方法,锁对象是字节码文件。
普通的同步方法,锁对象是this对象。
同步方法的锁是固定的。
总结:
1.同步代码块的锁可以是任意的对象,同步方法的锁是固定的。静态方法的锁对象是字节码文件,普通方法的锁对象是this对象。
2.锁对象必须是线程共享的,否则锁不住。
3.在同步代码块或同步方法中,调用Thread.sleep()方法,是不会释放锁对象的。调用wait()方法是会释放锁对象的。
售票问题代码实现:
package com.gupao.demo;
import java.util.concurrent.CountDownLatch;
public class Window extends Thread{
public Window(String name) {
super(name);
}
public static int tick_num=50;
@Override
public void run() {
while(true) {
synchronized ("锁") {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(tick_num>0) {
tick_num--;
System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩:"+tick_num);
}else {
System.out.println("没票了");
return;
}
}
}
public static void main(String[] args) {
Window a1=new Window("窗口1");
Window a2=new Window("窗口2");
Window a3=new Window("窗口3");
a1.start();
a2.start();
a3.start();
}
}
死锁
线程同步机制引发了死锁问题。
死锁问题出现的根本原因:
存在两个或两个以上的线程
存在两个或两个以上的共享资源
死锁问题的解决方案: 没有方案,只能尽量的去避免
代码模拟问题:
张三李四都去争抢遥控器和电池
public class App2 extends Thread{
public App2(String name) {
super(name);
}
@Override
public void run() {
if("张三".equals(this.getName())) {
synchronized ("电池") {
System.out.println("张三拿到了电池,准备去拿遥控器");
synchronized ("遥控器") {
System.out.println("张三拿到了电池和遥控器");
}
}
}
if("李四".equals(this.getName())) {
synchronized ("遥控器") {
System.out.println("李四拿到了遥控器,准备去拿电池");
synchronized ("电池") {
System.out.println("李四拿到了电池和遥控器");
}
}
}
}
public static void main(String[] args) throws IOException, InterruptedException, ParseException {
App2 a=new App2("张三");
App2 a2=new App2("李四");
a.start();
a2.start();
}
}
线程通信
wait()
一个线程执行wait方法,那么该线程会进入以锁对象为标识符的的线程池中。并且会释放锁资源
notify()
唤醒线程池中任何一个线程,那么该线程会唤醒以锁对象为标识符的线程池中的任意一个线程。
notifyAll(),唤醒锁对象中所有的线程。
注意事项:
wait和notify方法是属于Object对象的。
wait与notify方法必须在同步代码块或同步方法中使用。
wait和notiry必须由锁对象进行调用。
生产者生产产品,消费者去消费
代码实现:
public class Product {
String name;
double price;
public static void main(String[] args) {
Product t=new Product();
Customer c=new Customer(t);
Producter p=new Producter(t);
c.start();
p.start();
}
}
//生产者
class Producter extends Thread{
Product p;
public Producter(Product p) {
super();
this.p = p;
}
@Override
public void run() {
int i=0;
while(true) {
synchronized (p) {
if(i%2==0) {
p.name="苹果";
p.price=12;
}
if(i%2!=0) {
p.name="香蕉";
p.price=5;
}
i++;
System.out.println("生产者生产了"+p.name +",价格为"+p.price+"钱");
try {
p.notify();
p.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
//消费者
class Customer extends Thread{
Product p;
public Customer(Product p) {
super();
this.p = p;
}
@Override
public void run() {
while(true) {
synchronized (p) {
System.out.println("消费者买了"+p.name+",花了"+p.price+"钱");
try {
p.notify();
p.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
停止线程
t.stop() 停止线程,该方法已经过时,不安全的方法。
t.interrupt() 唤醒线程池中的线程,并且线程会接收到一个InterruptException 异常。
线程的停止:
停止一个线程一般通过变量去控制。
如果要停止一个阻塞状态的线程,还要使用变量配合notify()或interrupt()方法来控制。
package com.gupao.demo;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class Demo implements Runnable{
boolean flag=true;
@Override
public synchronized void run() {
int i=0;
while(flag) {
try {
this.wait();
} catch (InterruptedException e) {
System.out.println(e);
}
System.out.println(Thread.currentThread().getName()+":"+i);
i++;
}
}
public static void main(String[] args) {
Demo r=new Demo();
Thread t=new Thread(r,"线程1");
t.start();
for(int i=0;i<1000000;i++) {
if(i==100000) {
r.flag=false;
//方式一:使用notify,必须在同步方法中由锁对象调用
// synchronized (r) {
// r.notify();
// }
//方式二:使用interrupt
t.interrupt();
}
}
}
守护线程
守护线程(后台线程):在一个进程中,如果只剩下守护线程,那么守护线程也会死亡。
t.setDaemon(true) 设置该线程为守护线程。然后调用start()开启线程。【设置守护线程动作必须在start方法之前】
t.setDaemon(true)
t.start() 先设置守护标志,在开启
开启主线程和守护线程,此时JVM中自定义线程之后主线程和守护线程,当主线程执行完毕就会销毁,此时只剩下守护线程,然后守护线程也会死亡。
package com.gupao.demo;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class Demo extends Thread{
@Override
public void run() {
while(true) {
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) {
Demo d=new Demo();
d.setDaemon(true);
d.start();
for(int i=0;i<10000;i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
join线程
t2.join() 加入。一个线程执行该方法,那么就会有的线程加入。执行该语句的线程必须先让给新的线程执行。新的线程执行完毕才能继续执行。【线程让步】
t2.start();
t2.join(); //先开启线程在加入线程
package com.gupao.demo;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class App6{
public static void main(String[] args) {
Mother m=new Mother();
m.start();
}
}
class Mother extends Thread{
@Override
public void run() {
System.out.println("老妈做饭");
System.out.println("老妈发现没有酱油,通知儿子下楼买酱油");
try {
Son s=new Son();
s.start();
s.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("老妈得到酱油,继续做饭");
System.out.println("香喷喷的饭来了。。。");
}
}
class Son extends Thread{
@Override
public void run() {
System.out.println("儿子下楼");
System.out.println("儿子买酱油");
System.out.println("儿子买完酱油,儿子上楼");
}
}
控制台打印:
老妈做饭
老妈发现没有酱油,通知儿子下楼买酱油
儿子下楼
儿子买酱油
儿子买完酱油,儿子上楼
老妈得到酱油,继续做饭
香喷喷的饭来了。。。