一、线程的概念
(1)程序,进程和线程
程序pragam: 为解决某种问题,使用计算机语言编写的一系列指令(代码)的集合。本章中的程序,特指的是静态的,安装在硬盘上代码集合。
进程: 运行中的程序(被加载到内存中), 是操作系统进行资源分配的最小单位
线程: 进程可以进一步细化为线程,是进程内一个最小执行单元(具体要做的事情),是cpu进行任务调度的最小单位.
运行中的QQ 就是一个进程, 操作系统会为这个进程分配内存资源,一个聊天窗口就认为是一个线程, 这多个聊天窗口可以同时被cpu执行,但是这些聊天窗口属于进程, 线程是属于进程的. 早期没有线程,早期cpu执行的时候,是以进程为单位执行,进程单位还是比较大的,当一个进程运行时,其他的进行就不能执行,所以后来,将进程中的多个任务,细化为线程,cpu执行单位,也从进程转为更小的线程.
(2)进程和线程的关系
一个进程中可以包含多个线程 一个线程只能隶属于一个进程,线程不能脱离进程存在 一个进程中至少有一个线程(即主线程) java中的main方法,就是用来启动主线程 在主线程中可以创建并启动其他线程 所有线程都共享进程内存资源.
二、线程的创建与常用方法
(1)创建线程
创建线程共有三种方式
1、继承Thread类
写一个线程类,通过继承Thread类并重写run方法,并在程序中创建类并调用的方法进行使用
例:
myThread类
public class MyThread extends Thread{
/*
java中创建线程的方法
写一个类继承java.lang.Thread
重写run()
*/
@Override
public void run() {
/*
线程中要执行的任务都要写到run方法中,或者在run方法中进行调用
*/
for(int i=0;i<1000;i++){
System.out.println("MyThread"+i);
}
}
}
测试类:
import com.wbc.Thread.多线程.方式1继承Thread.MyThread;
public class threadTest {
public static void main(String[] args) {
//创建并启动线程
MyThread myThread =new MyThread();
//myThread.run();这不是启动线程,只是一个方法调用,本质仍是单线程
myThread.start();
for(int i=0;i<1000;i++){
System.out.println("main:"+i);
}
/*
MyThread941
MyThread942
MyThread943
main:957
main:958
main:959
main:960
MyThread944
MyThread945
MyThread946
MyThread947
*/
}
}
在需要开启线程的地方new继承了 Thread类的线程类
需要注意的是,使用 线程需要调用的是.start()方法而不是run方法,单纯调用run方法本质还是单线程调用
2、实现Runnable接口
创建一个任务类以实现Runnable接口,通过new Thread(任务类).start启动线程
例:
任务类:
public class Task implements Runnable{
/*
java中创建线程方式2
创建一个类实现Runnable接口
重写run方法
*/
@Override
public void run() {
for (int i=0;i<10000;i++){
System.out.println("task:"+i);
}
}
}
测试类:
package com.wbc.Thread.多线程.方式2实现接口;
public class TaskTest {
public static void main(String[] args) {
//创建任务
Task task =new Task();
//创建线程
Thread thread =new Thread(task);
thread.start();
for(int i=0;i<10000;i++){
System.out.println("main:"+i);
}
/*
main:9448
main:9449
main:9450
task:8954
task:8955
task:8956
task:8957
task:8958
task:8959
task:8960
main:9451
main:9452
*/
}
}
测试发现主线程(main)中for 循环打印的i和线程打印交替进行,证明是双线程
3、实现Callable接口
创建一个任务类以实现Callable接口并重写call方法
FutureTask futureTask =new FutureTask(任务类),创建将来执行任务的对象
new Thread(futureTask).start();
例:
任务类
import java.util.concurrent.Callable;
public class SumTask<T> implements Callable<T> {
@Override
public T call() throws Exception {
Integer i =0;
for(int j=0;j<100;j++){
i+=j;
}
return (T)i;
}
}
测试类:
package com.wbc.Thread.多线程.方式3实现Callable接口;
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 futureTask =new FutureTask(sumTask);
Thread thread =new Thread(futureTask);
thread.start();
try {
Integer i =(Integer) futureTask.get();//获取返回值
System.out.println(i);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
推荐使用实现Runnable接口进行创建线程
由于java是单继承的,继承了Thread类,就不能继承其他类了。实现Runnable接口避免了单继承的局限性 并且实现Runnable接口进行创建线程更适合多线程共享同一份资源的场景
当需要使用泛型时可以使用Callable接口
(2)线程的相关方法
Thread表示线程,提供了很多的方法,来对线程程进行控制. run(); 线程要执行的任务 start(); 启动java线程的 构造方法 new Thread(Runable runnablee); 接收一个任务对象.. new Thread(Runable runnablee,String name); 接收一个任务对象,并为线程设置名字 setName("我的线程1"); 为线程设置名字 String getName(); 获得线程的名字 Thread..currentThread();在任务中获得当前正在执行的线程 setPriority(int p11)设置优先级 1-10之间 默认是5 getPriority(); 获得优先级 sleep(long mm); 让线程休眠指定的时间 毫秒单位 join(); 让其他线程等待当前线程结束
myThread类
package com.wbc.Thread.多线程.ThreadMethods;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
/*
Thread中的方法:
1:run();用来定义线程要执行的任务代码
2:start();用来启动线程
3:Thread.currentThread();获取当前线程
4:getId();获取ID
5:getName();setName() 获取/设置名字
6:getState;获取线程状态
7:getPriority();setPriority();获取/设置线程优先级
优先级范围1——10;默认为5
8:Thread.sleep();休眠指定时间
9:join();等待当前线程执行完毕,其他线程再执行
10:Thread.yield();//礼让
*/
MyThread myThread =new MyThread();
myThread.setName("线程1");
myThread.setPriority(4);
myThread.start();
//myThread.join();//当代线程终止
MyThread myThread1 =new MyThread();
myThread1.setName("线程2");
myThread1.setPriority(3);
myThread1.start();
}
}
测试类:
package com.wbc.Thread.多线程.ThreadMethods;
public class MyThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(1000);//休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<1000;i++){
Thread thread =Thread.currentThread();//获取当前执行的线程
if(i%10==0){
Thread.yield();//礼让
}
System.out.println(thread.getName()+" "+i);
}
}
}
三、单线程与多线程
当在java程序中有几件不相关的事情同时有机会执行.可以在java中创建多个线程,把一些要执行的任务放在线程中执行,这样的话,都拥有让cpu执行的权利。而如果只需要顺序执行的话则不需要创建多个线程
在正式了解多线程之前先了解一下线程的生命周期
线程的生命周期
线程的生命周期是线程从创建到销毁的过程,期间共经历五个状态
新建状态:新建 new Thread(); 处于新建状态,此状态还不能被执行. 调用start()启动线程 让线程进入到就绪状态, 就绪状态:获得到cpu执行权后, 线程进入到cpu执行 运行状态:运行中的线程可以被切换,回到就绪状态, 也可能因为休眠等原因进入阻塞状态 阻塞状态:线程休眠时间到了 回到就绪状态 死亡状态:当线程中所有的任务执行完了, 线程也就自动销毁
线程生命周期示意图
多线程的概念及其优缺点
在一个程序可以创建多个线程,以执行不同的任务
多线程的优点:提高程序的响应 提高cpu的利用率 改善程序结构,将复杂任务分为多个线程,独立运行
多线程的缺点:线程越多占用内存越多 跟踪管理线程cpu开销变大
多线程的使用场景:
多线程访问操作同一个共享数据(例如 买票 、抢购、秒杀) 需要解决操作共享的问题。解决方法:排队+锁 在关键步骤处,多个线程只能一个一个执行
添加同步锁
加锁方式1:
使用synchronized关键字修饰代码块和方法
修饰代码块
同步对象要求: 多个线程用到的必须是同一个对象,可以是java中任何类的对象.
作用: 用来记录有没有线程进入到同步代码块中
synchronized(同步对象/锁){
//同步代码块, 一次只允许一个线程进入
}
以实现Runnable接口为例
任务类:
package com.wbc.Thread.多线程.模拟卖票2Runnable;
public class TicketTask implements Runnable{
int num =10;
@Override
public void run() {
while(true){
synchronized (this){//由于两个线程对应的是同一个任务对象,所以可以使用this,以及num可以不限制为静态的
if(num>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(
Thread.currentThread().getName()
+"买到了第"+num+"张票");
num--;
}
else{
break;
}
}
}
}
}
测试类:
package com.wbc.Thread.多线程.模拟卖票2Runnable;
public class TicketTest {
public static void main(String[] args) {
TicketTask task =new TicketTask();
Thread t1 =new Thread(task);
t1.setName("窗口1");
t1.start();
Thread t2 =new Thread(task);
t2.setName("窗口2");
t2.start();
}
}
修饰方法: 1.锁不需要我们提供了,会默认提供锁对象 2.synchronized如果修饰的是非静态的方法,锁对象是this synchronized如果修饰的是静态方法,锁对象是类的Class对象 一个类只有一个Class对象. 以继承Thread类为例:
线程类:
package com.wbc.Thread.多线程.模拟卖票1Thread;
public class TicketThread extends Thread{
static int num =10;//模拟票数有10张
static TicketThread tickerThread =new TicketThread();
/*
synchronized 修饰方法时,同步锁对象不需要指定
同步锁对象会默认提供:
1、非静态方法锁对象默认是this,会造成有多个this,两个对象
2、静态方法锁对象是当前类的class对象(类的对象,一个类的对象只有一个)
*/
@Override
public void run(){
while(true){
if(num<=0){
break;
}
print();
}
}
public static synchronized void print(){
if(num>0){
System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
num--;
}
}
}
测试类:
package com.wbc.Thread.多线程.模拟卖票1Thread;
public class TicketTest {
public static void main(String[] args) {
TicketThread t1 =new TicketThread();
t1.setName("张三");
t1.start();
TicketThread t2 =new TicketThread();
t2.setName("李四");
t2.start();
}
}
枷锁方式2:
使用jdk中提供的ReentrantLock类实现加锁 ReentrantLock只能对某一段代码块加锁,不能对整个方法加锁
线程类:
import java.util.concurrent.locks.ReentrantLock;
public class TicketThread extends Thread{
static int num =10;
//使用Thread需要静态保证只有一个锁对象,实现Runnable接口则不需要,因为只有一个任务对象
static ReentrantLock reentrantLock =new ReentrantLock();//加锁释放锁需要手动,如果出现异常不会自动释放,所以需要try{}finally{},确保锁会释放
@Override
public void run() {
while(true){
try{
reentrantLock.lock();//加锁,其底层状态由0转1;
if(num>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(
Thread.currentThread().getName()
+"买到了第"+num+"张票");
num--;
}
else{
break;
}
}finally {
reentrantLock.unlock();//释放锁,底层状态从1转0
}
}
}
测试类:
package com.wbc.Thread.多线程.模拟卖票3ReentrantLock;
public class TicketTest {
public static void main(String[] args) {
TicketThread t1 =new TicketThread();
t1.setName("张三");
t1.start();
TicketThread t2 =new TicketThread();
t2.setName("李四");
t2.start();
}
}
synchronized 和 ReentrantLock区别
相同点:都实现了加锁的功能 不同点: 1.synchronized 是一个关键字,ReentrantLock是一个类 2.synchronized修饰代码块和方法,ReentrantLock只能修饰代码块 3.synchronized可以隐式的加锁和释放锁,运行过程中如出现了异常可以自动释放 ReentrantLock需要手动的添加锁和释放锁,建议在finally代码块中释放锁.
四、线程通信问题
经典例题:生产者/消费者问题
生产者(Productor)将产品放在柜台(Counter),而消费者(Customer)从柜台处取走产品,生产者一次只能生产固定数量的产品(比如:1), 这时柜台中不能再放产品,此时生产者应停止生产等待消费者拿走产品,此时生产者唤醒消费者来取走产品,消费者拿走产品后,唤醒生产者,消费者开始等待
此类情况,需要线程与线程之间进行通信,而这种通讯在现阶段一般通过在同步代码块的基础上,使用wait,notify对线程进行控制
生产线程
package com.wbc.Thread.多线程.线程通信.生产者消费者问题;
public class ProductorThread extends Thread{
/*
生产者线程
*/
Counter counter;
public ProductorThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
while(true){
counter.add();
}
}
}
消费线程
package com.wbc.Thread.多线程.线程通信.生产者消费者问题;
public class CustomerThread extends Thread{
/*
消费者线程
*/
Counter counter;
public CustomerThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
while(true){
counter.sub();
}
}
}
柜台
package com.wbc.Thread.多线程.线程通信.生产者消费者问题;
public class Counter {
/*
柜台:共享数据
*/
int num = 0;//商品的数量
/*
只有一个Counter对象,两个方法都非静态,但用的都是同一把锁
*/
//负责生产商品的方法
public synchronized void add(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(num==0){
num=1;
this.notify();//唤醒消费者线程
System.out.println(Thread.currentThread().getName()+":已生产");
}
else{
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//负责销售的方法
public synchronized void sub(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(num==1){
num=0;
this.notify();
System.out.println(Thread.currentThread().getName()+":已购买");
}
else{
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/*
Thread.sleep(long time);
属于Thread类中的方法
sleep();让线程休眠之后会自动唤醒,并继续排队准备执行
sleep()不会释放锁
obj.wait();
属于Object类的中方法,需要锁对象调用
wait后的线程需要其他线程唤醒(notify();notifyAll();唤醒)
wait()会自动释放锁
*/
运行类
package com.wbc.Thread.多线程.线程通信.生产者消费者问题;
public class Test {
public static void main(String[] args) {
Counter counter =new Counter();//唯一的Counter对象
CustomerThread customerThread =new CustomerThread(counter);
customerThread.setName("消费者");
ProductorThread productorThread =new ProductorThread(counter);
productorThread.setName("生产者");
customerThread.start();
productorThread.start();
}
}