多线程
程序:是为了特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生,存在和消亡的过程。-----生命周期 比如运行中的QQ,运行中的音乐播放器
程序是静态的,进程是动态的。
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的。
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)。线程切换的开销小
一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简洁,高效。但多个线程操作共享的系统资源可能就会带来安全隐患。
并行:多个CPU同时执行多个任务。
并发:一个CPU(采用时间片)同时执行多个任务。
以单核CPU为例,只使用单个线程先后完成多个任务,肯定比多个线程来完成用的时间更短,为何仍需多线程呢?
多线程的优点:
提高应用程序的响应,对图形化界面更有意义,可增强用户体验。
提高计算机系统CPU的利用率。
改善程序结构。将即长又复杂的进程分为多个线程,独立运行,利于理解和修改。
多线程的创建
方式一:继承于Thread类
创建一个继承于Thread类的子类
重写Thread类的run()
创建Thread类的子类的对象
通过此对象调用start()
说明:不能通过调用run方法启动线程
不可让已经start()的线程再start();需要新建一个实例对象。
另一种写法
创建Thread类的匿名子类对象
方式二:实现Runnable接口
创建类实现Runnable接口
实现类去实现Runnable中的抽象方法:run()
创建类的实例对象
将该实例对象作为参数传递到Thread类的构造器中,创建Thread类的对象
通过Thread类的对象调用start()
package test1;
public class ThreadTest {
public static void main(String[] args) {
Mythread t = new Mythread();
Thread t1 = new Thread(t);
t1.start();
}
}
class Mythread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " - 开始!");
}
}
比较创建线程的两种方式
开发中:优先选择实现Runnable接口的方式
原因:实现的方式没有类的单继承局限性
实现的方式更适合来处理多个线程有共享数据的情况
联系:Thread也是Runnable的实现类,继承Thread的类变相也实现了Runnable接口
相同点:两者方式都需要重写Run(),将线程要执行的逻辑声明在Run()中。
JDK5.0之后新增的线程创建方式
方式一:实现Callable接口
创建实现Callable接口的实现类
实现call方法,将线程要做的操作声明在此方法中
创建实现Callable接口的实现类的实例
将此对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread的对象,调用start()
可以通过FutureTask的对象的get()方法得到call()方法的返回值
package test1;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
public static void main(String[] args) {
MyThread t1 = new MyThread();
FutureTask futureTask = new FutureTask(t1);
new Thread(futureTask).start();
try {
Object sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
与实现Runnable相比,Callable功能更强大
相比run()方法,重写的call()方法的差异
call()方法相比run()方法,可以有返回值
call()方法可以抛异常
call()方法支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
方式二:线程池(常用)
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现反复利用。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
便于线程管理
步骤:
提供指定线程数量的线程池
设置线程池属性
执行指定的线程的操作,需要提供实现Runnable或实现Callable接口实现类的对象
关闭连接池
package test1;
import java.util.concurrent.*;
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//设置线程池属性
ThreadPoolExecutor service1= (ThreadPoolExecutor) service;
service1.setCorePoolSize(5);
service1.setMaximumPoolSize(10);
//执行指定的线程的操作
//适合Runnable
service.execute(new Mythread1());
//适合Callable
service.submit(new Mythread2());
service.shutdown();
}
}
class Mythread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(i);
}
}
}
class Mythread2 implements Callable {
@Override
public Object call() throws Exception {
for (int i = 100; i >= 0; i--) {
System.out.println(i);
}
return 1;
}
}
Thread类的有关方法
start():启动当前线程;调用当前线程的run() run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中 currentThread():静态方法,返回执行当前代码的线程 getName():获取当前线程的名字 setName():设置当前线程的名字 yield():释放当前CPU的执行权 join():在线程a中调用线程b的join方法,线程a进入阻塞状态,直到线程b执行结束 stop()(已过时):强制结束当前进程 sleep(long millitime):让当前线程“睡眠”指定时间,在指定时间内,线程为阻塞状态 isAlive():判断当前线程是否还存活
package test1;
public class ThreadTest {
public static void main(String[] args) {
TestThread t1 = new TestThread();
t1.setName("线程1");
TestThread t2 = new TestThread("线程2");
t1.start();
t2.start();
//给主线程命名
Thread.currentThread().setName("主线程");
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i == 20) {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(t2.isAlive());
}
}
class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i % 20 == 0) {
//this.yield();
try {
this.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public TestThread() {
}
//提供构造器给线程命名
public TestThread(String name) {
super(name);
}
}
线程的调度
线程的优先级
MAX_PRIORITY:10
MIN_PRIORITY:10
NORM_PRIORITY:5
高优先级的线程要抢占低优先级线程cpu的执行权,但是只是从概率上讲,高优先级的线程高概率被执行,但并不意味着只有当高优先级的线程执行完后,低优先级的线程才能执行。
package test1;
public class ThreadTest {
public static void main(String[] args) {
TestThread t1 = new TestThread();
t1.setName("线程1");
TestThread t2 = new TestThread("线程2");
//给主线程命名
Thread.currentThread().setName("主线程");
//获取线程优先级 均为5
System.out.println(t1.getPriority());
System.out.println(t2.getPriority());
System.out.println(Thread.currentThread().getPriority());
//设置线程优先级
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
t1.setPriority(Thread.NORM_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
}
class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public TestThread() {
}
//提供构造器给线程命名
public TestThread(String name) {
super(name);
}
}
线程的生命周期
线程的五种状态
新建:当一个Thread类或其子类的对象被声明被创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时他已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊的情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡:线程完成了他的全部工作或线程被提前强制性的中止或出现异常导致结束
线程的安全问题
例:创建三个窗口卖票,总票数100张,使用实现Runnable接口的方式
问题:卖票过程中,出现了重票,错票 -->出现了线程安全问题
问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
如何解决:当线程a在操作ticket的时候,其他线程不能参与进来,直到线程a操作完成后,其他线程才可以操作ticket。这种情况即使线程a出现了阻塞,其他线程也不能改变。
在Java中我们通过同步机制,来解决线程的安全问题
方式一:同步代码块
用法:
synchronized(同步监视器){
//需要被同步的代码
}
说明:操作共享数据的代码,即为需要被同步的代码(包含的代码要合理)
共享数据:多个线程共同操作的变量
同步监视器:俗称:锁。任何一个类的对象, 都可以充当锁。
要求:多个线程必须要公用同一把锁。(实现Runnable接口的方式可以使用this充当锁;实现Runnable和继承Thread两种都可以用"当前类名.class"充当锁)
同步的方法解决线程安全问题的(好处)
操作同步代码块时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低(局限性)
解决实现Runnable接口的线程安全问题
package test1;
public class ThreadTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " - 第" + ticket + "张");
ticket--;
} else {
break;
}
}
}
Thread.yield();
}
}
解决继承Thread的线程安全问题
package test1;
public class ThreadTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread {
private static int ticket = 100;
static Object obj = new Object();
@Override
public void run() {
while (true) {
//也可以使用Window.class充当锁
synchronized (obj) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " - 第" + ticket + "张");
ticket--;
} else {
break;
}
}
}
Thread.yield();
}
}
和解决Runnable的方法的区别在于实现Runnable接口的锁是同一把,继承Thread方式的锁需要加Static才是同一把
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,不妨将此方法声明为同步的。
解决实现Runnable接口的线程安全问题
package test1;
public class ThreadTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
sellTicket();
if (sellTicket() == 1) {
break;
}
}
}
public synchronized int sellTicket() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " - 第" + ticket + "张");
ticket--;
} else {
return 1;
}
Thread.yield();
return 0;
}
}
解决继承Thread的线程安全问题
将ticket和同步方法改为静态即可
非静态的同步方法,同步监视器为this;
静态的同步方法,同步监视器为当前类本身
单例模式之懒汉式的线程安全
class Bank {
private static Bank instance = null;
private Bank() {
}
public static Bank getInstance() {
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
可能出现死锁的例子
package test1;
public class ThreadTest {
public static void main(String[] args) {
StringBuffer a = new StringBuffer();
StringBuffer b = new StringBuffer();
new Thread() {
public void run() {
synchronized (b) {
a.append("a");
System.out.println(a);
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
a.append("b");
System.out.println(a);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (a) {
a.append("c");
System.out.println(a);
synchronized (b) {
a.append("d");
System.out.println(a);
}
}
}
}).start();
}
}
方式三:Lock 锁
从JDK 5.0开始,Java提供了更强大的线程同步机制--通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。所提供了对共享资源的独占访问,每次只能由一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得的Lock对象。
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁,释放锁。
package test1;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - 第" + ticket + "张");
ticket--;
} else {
break;
}
} finally {
lock.unlock();
}
Thread.yield();
}
}
}
synchronized和Lock的异同
同:二者都用来解决线程安全问题
异:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
Lock需要手动的启动同步,同时结束同步也需要手动的实现(unlock)
线程的通信
两线程交互执行
public void run() {
while (true) {
synchronized (this) {
notify();
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - 第" + ticket + "张");
ticket--;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
涉及到的方法(这些方法只能出现在同步代码块或同步方法中,这些方法的调用者是同步代码块或同步方法的同步监视器)
wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器
notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那一个
notifyAll():一旦执行此方法,就会唤醒所以被wait的线程
这三个方法定义在java.lang.Object类中,因为同步监视器可以是任何一个类的对象
sleep和wait的异同
同:都可以是当前线程进入阻塞状态
异:两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait();
调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或方法
关于是否释放同步监视器:在同步代码块/方法中调用sleep()不会释放锁,wait()会释放锁