目录
一、程序,进程,线程
程序(program):为了完成特定任务,用某种语言编写的一组指令(代码)的集合。是一段静态的代码
进程(process):是正在内存中运行的应用程序,如后台的QQ,后台的音乐。进程是操作系统进行资源分配的最小单位。
线程(thread):是一个进程内部的最小执行单位,是操作系统进行任务调度的最小单位,隶属于进程。
1.线程和进程的关系
● 一个进程可以包含多个线程,
● 一个线程只能属于一个进程,线程不能脱离进程而独立运行;
● 每一个进程至少包含一个线程(称为主线程);
● 在主线程中可以创建并启动其它的线程;
● 一个进程内的所有线程共享该进程的内存资源。
二、创建线程
1.继承Thread类
在Java中要实现线程,最简单的方式就是扩展Thread类,重写其中的run方法,方法如下:
//定义:
pubilc class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 10000; i++) {
System.out.println("main"+i);
}
}
}
//创建Thread子类对象(MyThread类)
MyThread mythread = new MyThread();
//mythread.run();不能直接调用run(), 直接调用是一个普通方法调用,并不是启动线程
mythread.start();//启动线程,启动线程后,并不是立刻执行的,需要操作系统的调度
2.实现Runnable接口
java.lang.Runnable接口中仅有一个抽象方法:
public void run()
也可以通过实现Runnbale接口的方式来实现线程,只需实现其中的run方法即可;
Runnable接口的存在主要是为了解决Java中不允许多继承的问题。
实现Runnable接口的方式:
定义:
public class MyTask implements Runnable{
/*
创建一个任务,实现Runnable接口
重写Runnable中的run方法
*/
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("myTask"+i);
}
}
}
线程执行任务:
public class Test {
/*
main方法用来启动Java主线程
*/
public static void main(String[] args) {
//创建一个实现Runanble接口的对象(MyTask类)
MyTask myTask = new MyTask();
//创建了一个线程 把myTask放入Thread
Thread thread = new Thread(myTask);
thread.start();//启动线程 执行myTask
for (int i = 0; i < 10000; i++) {
System.out.println("main"+i);
}
}
}
并发问题
并发问题产生的原因:多个线程同时访问了共享资源,并对共享资源做了相应的处理。
场景:有三个人在不同的窗口同时卖10张票
public class ThreadTest implements Runnable {
private int ticketNums = 10;
@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}
// 模拟延迟
try {
Thread.sleep(1000);//sleep()方法用来使正在执行的线程暂停指定的毫秒数
System.out.println(Thread.currentThread().getName() + "----->卖了第" + ticketNums-- + "张票");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class TestTicket {
public static void main(String[] args) {
//创建实例化Runnable对象的类
ThreadTest threadTest = new ThreadTest();
//放入Thread的构造方法中
Thread t1 = new Thread(threadTest,"小红");
Thread t2 = new Thread(threadTest,"小明");
Thread t3 = new Thread(threadTest,"小王");
t1.start();
t2.start();
t3.start();
}
}
运行程序后,发现有人卖了相同的票
这就是并发问题。
实现Runnable接口方式的好处:
1.避免了单继承的局限性,继承了Thread类,就不能继承其他类
2.多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
三、Thread类中的方法
1.常用方法
run();线程要执行的任务
构造方法:
new Thread(Runnable runnable);接受一个任务对象
new Thread(Runnable runnbale,String name);接收一个任务对象,并为线程设置名字
setName("//线程名字");为线程设置名字
getName();获得线程的名字
Thread.currentThread();在任务中获得当前正在执行的线程
2.线程优先级
计算机只有一个CPU,各个线程轮流获得CPU的使用权,才能执行任务;
优先级高的线程会有更多机会获得CPU;
优先级用整数表示,取值范围是1~10,一般情况下,线程的默认值都是5,但是也可以通过setPriority和getPriority方法来设置或返回优先级;
操作系统线程任务调度算法
●先来先服务(FCFS)调度算法
●短作业优先(SJF)调度算法
●优先级调度算法
●时间片轮转调度算法
●高响应比优先调度算法
●多级反馈队列调度算法(集合了前几种算法的优点)
Java的调度方法
●同优先级线程组成先进先出队列,使用时间片策略
●对高优先级,使用优先调度的抢占式策略
Thread类有如下3个静态常量来表示优先级
●MAX_PROIRITY:取值为10,表示最高优先级。
●MIN_PROIRITY:取值为1,表示最底优先级。
●NORM_PROIRITY:取值为5,表示默认的优先级。
四、线程状态
线程在它的生命周期中会处于不同的状态:
线程的状态:
1.新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
2.就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
3.运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
4.阻塞:在某种特殊情况下,被人为挂起(suspend())或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。(sleep()和join()也可以使线程由运行状态进入阻塞状态)
5.死亡:线程完成了它的全部工作或线程被提前强制性地中止或(stop()或destory())出现异常导致结束
五、多线程的概念
多线程是指程序中包含多个执行单元,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
何时需要多线程
1.程序需要同时执行两个或多个任务
2.程序需要实现一些需要等待的任务时,如客户输入,文件读写操作,网络操作,搜索等。
多线程的例子
1.售票系统:在火车站或电影院等场所,多个售票窗口需要同时出售相同数量的票。
2.Web服务器处理请求:Web服务器需要同时处理来自多个客户端的请求。
- Web服务器内部采用多线程机制,为每个客户端请求分配一个或多个线程来处理。
多线程的优点
1.提高程序的响应
2.提高CPU的利用率
3.改善程序结构,将复杂任务分为多个线程,独立运行
多线程的缺点
1.线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
2.多线程需要协调和管理,所以管理跟踪管理线程,使得CPU开销变大;
3.线程之间同时对对共享资源的访问会相互影响,如果不加以控制会导致数据出错。
六、线程同步
1.多线程同步
多个线程同时读写同一份共享资源时,可能会引起冲突。所以引入线程“同步”机制,即各线程间要有先来后到;
2.同步就是排队+锁
几个线程之间要排队,一个个对共享资源进行操作,而不是同时进行操作;
为了保证数据在方法中被访问时的正确性,在访问时加入锁机制
确保一个时间点只有一个线程访问共享资源。可以给共享资源加一把锁,哪个线程获取了这把锁,才有权利访问该共享资源。
在Java代码中实现同步:
使用synchronized(同步锁)关键字同步方法或代码块。
synchronized (同步锁){
// 需要被同步的代码;
}
synchronized还可以放在方法声明中,表示整个方法,为同步方法。
例如:
public synchronized void show (String name){
// 需要被同步的代码;
}
用窗口一,二一同卖10张票举例:
public class TicketThread extends Thread{
static int t = 10;
Object s = new Object();
@Override
public void run() {
while (true) {
/*
synchronized修饰代码块
同步对象: 对多个线程对应的对象必须是同一个,
同步对象可以是java中任何类的对象
用来记录有没有线程进入到同步代码块中的,在对象的对象头中有一块空间用来记录有没有线程进入到同步代码块
synchronized(同步对象){
同步代码块
}
*/
synchronized (s) {
if (t > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买到了" + t--+"票");
}
}
}
}
}
public class TestLock {
public static void main(String[] args) {
TicketThread t1 = new TicketThread();
t1.setName("窗口1");
TicketThread t2 = new TicketThread();
t2.setName("窗口2");
t1.start();
t2.start();
}
}
同步锁
同步锁可以是任何对象,必须唯一,保证多个线程获得是同一个对象(用来充当锁标记)
/*
卖票的线程
*/
public class TicketThread extends Thread{
static int t = 10;//剩余10张票
@Override
public void run() {
while (true){
if(t<=0){
break;
}
TicketThread.printTicket();//调用出票方法
}
}
/*
出票的方法
synchronized修饰方法,同步对象会有默认的.
synchronized如果修饰的是非静态方法,那么同步对象就是this
synchronized如果修饰的是静态方法,那么同步对象就是当前类的Class对象.
当前类的Class对象: 一个加载到内存后,会为这个创建一个位于的Class类对象
*/
public static synchronized void printTicket(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(t>0){
System.out.println(Thread.currentThread().getName()+"卖了"+t);
t--;
}
}
}
同步执行过程
1.第一个线程访问,锁定同步对象,执行其中代码
2.第二个线程访问,发现同步对象被锁定,无法访问
3.第一个线程访问完毕,解锁同步对象
4.第二个线程访问,发现同步对象没有锁,然后锁并访问
七、Lock(锁)
从JDK 5.0开始,同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
ReentrantLock类实现了Lock,它拥有于synchronized相同的并发性和内存语义,在实现线程安全的控制中,可以显式加锁释放锁。
用ReentrantLock类实现加锁
ReentrantLock只能对某一段代码加锁,不能对整个方法加锁。示例:
public class TicketTask implements Runnable {
static int num = 10;
ReentrantLock reentrantLock = new ReentrantLock();//提供加锁对象
@Override
public void run() {
while (true) {
reentrantLock.lock();//加锁
try {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "卖了" + num--);
} else {
break;
}
} finally {//在finally块中保证锁必须释放
reentrantLock.unlock();//释放锁
}
}
}
}
public class Test {
public static void main(String[] args) {
TicketTask ticketTask = new TicketTask();
Thread t1 = new Thread(ticketTask,"t1");
Thread t2 = new Thread(ticketTask,"t2");;
t1.start();
t2.start();
}
}
synchronized和ReentrantLock区别
相同点:都实现了加锁的功能
不同点:synchronized是一个关键字,ReentrantLock是一个类
synchronized可以修饰代码块和方法,ReentrantLock只能修饰代码块
synchronized可以隐式的枷锁和释放锁,运行过程中如果出现了异常可以自动释放。ReentrantLock需要手动的加锁和释放锁,建议在finally代码块中释放锁以保证锁被释放掉。
八、线程通信
线程通讯指的是多个线程通过相互牵制,相互调度,即线程之间的相互作用。
涉及三个方法:
.wait:一旦执行此方法,当前线程就进入阻塞状态,并释放同步锁对象。
.notify:一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
.notifyALL:一旦执行此方法,就会唤醒所有被wait的线程
.wait() .notify() .notigyALL()三个方法必须使用在同步代码块或同步方法中。
用以上方法可以让两个线程交替进行。示例代码:
public class PrintNumThread extends Thread {
static int num = 0;
static Object object = new Object();
/*
wait(); notify(); notifyALL();
这三个方法都是Object类中定义的方法,
这三个方法必须在同步代码块中使用,
这三个方法必须通过为锁的对象调用
*/
@Override
public void run() {
while (true) {
synchronized (object) {
object.notify();//唤醒等待中的线程 Object充当锁对象
if (num <= 100) {
num++;
System.out.println(Thread.currentThread().getName() + ":" + num);
} else {
break;
}
try {
object.wait();//让线程等待,同时释放了锁,等待的线程不能自己醒来,必须让另一个线程唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Test {
public static void main(String[] args) {
PrintNumThread p1 = new PrintNumThread();
p1.setName("线程1");
PrintNumThread p2 = new PrintNumThread();
p2.setName("线程2");
p1.start();
p2.start();
}
}
运行结果:
经典例题:生产者/消费者问题
生产者(Productor)将产品放在柜台(Counter),而消费者(Customer)从柜台 处取走产品,生产者一次只能生产固定数量的产品(比如:1), 这时柜台中不能 再放产品,此时生产者应停止生产等待消费者拿走产品,此时生产者唤醒消费者来 取走产品,消费者拿走产品后,唤醒生产者,消费者开始等待.
public class Counter {
int num = 1; //代表商品的数量 例如为0
//负责生产商品的方法 锁对象是this add() 和 sub() 用的是同一把锁
public synchronized void add(){
if(num==0){
System.out.println("生产者生产了一件商品");
num = 1;//生产一件商品
this.notify();//唤醒消费者线程
}else{
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//负责消费商品的方法
public synchronized void sub(){
if(num>0){
System.out.println("消费者拿走了一件商品");
num=0;//消费者拿走了商品
this.notify();
}else{
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class CustomerThread extends Thread{
Counter counter;
public CustomerThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
while (true){
counter.sub(); //消费者线程一直消费
}
}
}
public class ProductorThread extends Thread{
Counter counter;
public ProductorThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
while (true){
counter.add();//生产者线程 一直生产
}
}
}
public class Test {
public static void main(String[] args) {
Counter counter = new Counter();//创建的唯一的一个柜台对象
ProductorThread p = new ProductorThread(counter);
CustomerThread c = new CustomerThread(counter);
p.start();
c.start();
}
}
九、新增创建线程方式
实现Callable(调用)接口与使用Runnable相比,Callable功能更强大些。Callable接口优点:
1.相比run()方法,可以有返回值。
2.方法可以抛出异常
3.支持泛型的返回值
4.需要借助FutureTask类,获取返回结果
如下:
接收任务
FutureTask<Integer> futureTask = new FutureTask(任务);
创建线程
Thread t = new Thread(futureTask);
t.start();
Integer val = futureTask.get();获得线程call方法的返回值
实现Callable接口:
重写call()方法
call()可以有返回值,可以抛出异常,还可以自定义返回结果的类型:
代码示例:
public class SumTask<T> implements Callable<T> {
@Override//重写call()方法并抛出异常
public T call() throws Exception {
Integer i = 0;
for (int j = 0; j < 10; j++) {
i+=j;
}
return (T)i;
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) {
SumTask<Integer> sumTask = new SumTask<>();
FutureTask<Integer> futureTask = new FutureTask(sumTask);
Thread thread = new Thread(futureTask);
thread.start();
try {
Integer integer = futureTask.get();//获取到线程的执行结果
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}