文章目录
一、线程与进程
在没有使用多线程的程序中,程序只有一个主线程我们的程序是根据代码自上而下的。
而在多线程程序中,其他的线程与主线程同时运行。
如图:
接下来我们介绍相关概念
说起进程,就不得不说程序
1、程序
程序是指令和数据的有序集合,其本身没有任何运行含义,是一个静态的概念
2、进程
而进程是一个动态的概念,每个运行中的程序就是一个进程,是系统进行资源分配和调度的一个独立单元。
3、线程
通常在一个进程中可以包含若干线程,当然一个进程至少有一个线程,线程是CPU调度和执行的单位
进程是线程的容器,一个进程至少有一个线程。
每个线程都有各自的线程栈,自己的寄存器环境。
4、注意
①进程A与进程B资源不共享
②在java中:线程A与线程B,堆内存与方法区共享,线程的栈独立存在,一个线程一个栈。
③java中之所以有多线程,是为了提高程序的处理效率
④当main方法结束后,其他线程可能依然在运行
5、多线程编程的优势:
①进程之间不能共享内存,但线程之间共享内存非常容易。
②系统创建进程时需要为该进程重新分配系统资源,但创建线程的代价小得多,因此使用多线程来实现多任务并发比多进程效率高。
③Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。
二、多线程的实现
1、继承Thread类
继承Thread类,重写run()方法,调用start开启线程
//继承Thread类的方式实现多线程
public class Thread1 extends Thread{
public void run() {
for(int i=0;i<20;i++) {
System.out.println("World"+i);
}
}
public static void main(String[] args) {
new Thread1().start();
for(int i=0;i<100;i++) {
System.out.println("Hello"+i);
}
}
}
注:线程开启不一定立即执行,由CPU调度执行
2、实现Runnable接口
定义类实现Runnable接口,实现run()方法编写线程执行体,创建线程对象,调用start()方法启动线程
public class TestThread2 implements Runnable{
@Override
public void run() {
for(int i=0;i<20;i++){
System.out.println("World==="+i);
}
}
public static void main(String[] args) {
//创建runnable接口的实现对象
TestThread2 testThread2=new TestThread2();
//创建线程对象,通过线程对象来开启我们的线程,代理(静态代理)
new Thread(testThread2).start();
for(int i=0;i<100;i++){
System.out.println("Hello==="+i);
}
}
}
推荐使用Runnable对象,避免java单继承的局限性,方便同一个对象被多个线程使用。
3、实现Callable接口(了解即可)
public class TestCallable implements Callable {
@Override
public Boolean call() throws Exception {
for(int i=0;i<20;i++){
System.out.println("World "+i);
}
return true;
}
public static void main(String[] args) {
TestCallable testCallable=new TestCallable();
//创建执行服务
ExecutorService ser= Executors.newFixedThreadPool(1);
//提交执行
Future<Boolean> r1=ser.submit(testCallable);
//主线程执行
for(int i=0;i<100;i++){
System.out.println("Hello "+i);
}
//获取结果
try {
boolean rs1=r1.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//关闭服务
ser.shutdown();
}
}
三、线程执行状态与方法
1、Thread.sleep(long millis)线程休眠
- 每个线程都有一把锁,sleep不会释放锁
当前线程进入阻塞,持续millis毫秒
模拟倒计时:
//模拟倒计时
//打印当前系统时间
public class TestSleep {
public static void main(String[] args) {
Date startTime=new Date(System.currentTimeMillis());
for(int i=10;i>=0;i--){
try {
Thread.sleep(1000);//主线程休眠1秒
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime=new Date(System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2、线程停止
- 建议使用次数,避免死循环
- 建议使用标志位
- 不建议使用JDK废弃的方法如stop、destory等
设置标志位使线程停止:
public class TestStop implements Runnable{
//设置一个标志位
private boolean flag=true;
@Override
public void run() {
int i=0;
while(flag){
System.out.println("run......thread======"+i++);
}
}
public void stop(){
this.flag=false;
}
public static void main(String[] args) {
TestStop testStop=new TestStop();
new Thread(testStop).start();
for(int i=0;i<1000;i++){
System.out.println("main===="+i);
if(i==900){
testStop.stop();
System.out.println("线程停止");
}
}
}
}
3、Thread.yield()线程礼让
让当前正在执行的线程暂停,但不阻塞
将线程从运行态转为就绪态
让CPU重新调度,礼让不一定能成功(看概率)
public class TestYield{
public static void main(String[] args) {
MyYield myYield1=new MyYield();
new Thread(myYield1,"a").start();
new Thread(myYield1,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始===");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程结束===");
}
}
礼让失败:
a线程开始===
a线程结束===
b线程开始===
b线程结束===
礼让成功:
a线程开始===
b线程开始===
a线程结束===
b线程结束===
4、Thread.join()线程强制执行
join合并线程,待此线程完成后再执行其他线程,即将其他线程阻塞,可以想象成插队
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i=0;i<1000;i++){
System.out.println("VIP==="+i);
}
}
public static void main(String[] args) {
TestJoin testJoin=new TestJoin();
Thread thread=new Thread(testJoin);
thread.start();
//主线程
for(int i=0;i<500;i++){
if(i==100){
try {
thread.join(); //插队(强制执行,容易产生阻塞)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("主线程==="+i);
}
}
}
当主线程循环达到100次时,会让thread线程先运行完。
5、守护线程
Java线程分为用户线程和守护线程。
守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。
Java中把线程设置为守护线程的方法: setDaemon(true) 方法。
public class TestDaemon implements Runnable{
@Override
public void run() {
while(true){
System.out.println("守护线程运行===");
}
}
public static void main(String[] args) {
TestDaemon testDaemon=new TestDaemon();
Thread thread=new Thread(testDaemon);
thread.setDaemon(true); //设置为守护线程
thread.start();
for(int i=0;i<10;i++){
System.out.println("主线程(用户线程)运行==="+i);
}
System.out.println("主线程退出===");
}
}
从打印结果可以看出,在主线程结束之后,守护线程等jvm退出之后才停止运行
注意:
- setDaemon(true) 必须在 start()之前设置,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程,继续执行
- 守护线程创建的线程也是守护线程
- 守护线程不应该访问、写入持久化资源,如文件、数据库,因为它会在任何时间被停止,导致资源未释放、数据写入中断等问题
四、线程同步
在介绍线程同步之前我们先来讲一下什么是线程不安全
我们模拟了一个线程不安全的买票过程
//不安全的买票
//每个线程都在自己的工作内存交互,内存控制不当会造成数据不一致
public class UnsafeBuyTickets {
public static void main(String[] args) {
Station station=new Station();
new Thread(station,"小明").start();
new Thread(station,"小红").start();
new Thread(station,"ben").start();
}
}
class Station implements Runnable{
//票
static int tickets=10;
//标志位
static boolean flag=true;
@Override
public void run() {
while(flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void buy() throws InterruptedException {
if(tickets<=0){
flag=false;
return;
}
//买票
System.out.println(Thread.currentThread().getName()+"买到了"+tickets--);
//模拟延时
Thread.sleep(100);
}
}
小明买到了10
ben买到了9
小红买到了8
小明买到了7
ben买到了6
小红买到了7
ben买到了5
小红买到了5
小明买到了4
小红买到了3
ben买到了2
小明买到了3
ben买到了1
小红买到了0
小明买到了-1
最后的结果出现了负数,明显错了。
出现该错误的原因:由于统一进程的多个线程共享一块存储空间,在带来方便的同时,也带来了访问冲突的问题,如两个线程同时访问该资源
1、synchronized关键字
- 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法synchronized方法和synchronized块。
- synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到方法返回才释放。
我们将上述不安全的买票过程改写一下,加上synchronized关键字:
①同步方法
//安全的买票
public class SafeBuyTickets {
public static void main(String[] args) {
Station1 station1=new Station1();
new Thread(station1,"小明").start();
new Thread(station1,"小红").start();
new Thread(station1,"ben").start();
}
}
class Station1 implements Runnable{
//票
static int tickets=10;
//标志位
static boolean flag=true;
@Override
public void run() {
while(flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//同步锁,锁的是this
public synchronized void buy() throws InterruptedException {
if(tickets<=0){
flag=false;
return;
}
//模拟延时
Thread.sleep(15);
//买票
System.out.println(Thread.currentThread().getName()+"买到了"+tickets--);
}
}
结果为:
小明买到了10
小红买到了9
小红买到了8
小红买到了7
小红买到了6
小红买到了5
小红买到了4
小红买到了3
ben买到了2
ben买到了1
锁机制也存在一下问题
- 一个线程持有锁会导致所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会产生比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
②同步块
我们知道ArrayList是线程不安全的集合
public class UnsafeArrayList {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
for(int i=0;i<10000;i++){
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
在list里的对象数本应该是10000个,但出现了线程不安全
9999
此时我们用synchronized对list加锁,即把list添加对象的语句用同步块包住
public class SafeArrayList {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
for (int i=0;i<10000;i++){
new Thread(() -> {
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
list大小为10000
10000
2、Lock(锁)
- 从JDK5.0开始,Java提供了强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口时控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个现成对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。
用Lock实现同步锁买票过程
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2=new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable{
int ticketNum=10;
//定义一把锁
ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while(true){
try{
lock.lock(); //加锁
if(ticketNum>0){
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(ticketNum--);
}else{
break;
}
}finally {
lock.unlock(); //释放锁
}
}
}
}
3、synchronized与Lock对比
- Lock是显式锁(手动开启和关闭),synchronized是隐式锁,出了作用域自动释放。
- Lock是有代码块锁,synchonrized有代码块锁和方法锁。
- 使用Lock锁,JVM将花费较少的时间来调度,性能更好。并且有更多的扩展性
- 优先使用顺序:Lock>同步代码块>同步方法
五、线程通信
1、生产者消费者问题
在讲该部分之前我们需要先介绍一个经典线程通信问题:“生产者消费者问题”。
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
.
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。
- 假设仓库只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品位置
分析:生产者消费者共享一个资源,并且他们之间相互依赖,互为条件
- 对于生产者,没有生产产品之前,要通知消费者等待;在生产了产品之后又要通知消费者消费
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品
2、解决线程通信问题的方法
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用
3、管程法解决生产者消费者问题
并发协作模型—>管程法
生产者生产的产品放入缓冲区,消费者从缓冲区拿出产品
//生产者消费者模型,管程法
public class PcTest {
public static void main(String[] args) {
SynContainer container=new SynContainer();
Productor productor=new Productor(container);
Comsumer comsumer=new Comsumer(container);
productor.start();
comsumer.start();
}
}
//生产者
class Productor extends Thread{
private SynContainer container;
public Productor(SynContainer container){
this.container=container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Product(i));
System.out.println("生产者生产了第"+i+"个产品===");
}
}
}
//消费者
class Comsumer extends Thread{
private SynContainer container;
public Comsumer(SynContainer container){
this.container=container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费者消费了第"+container.pop().id+"件产品--->");
}
}
}
//产品
class Product{
public int id;
public Product(int id){
this.id=id;
}
}
//安全的缓冲区
class SynContainer{
Product[] products=new Product[10];
//容器内产品的数量
static int count=0;
//生产者放入产品
public synchronized void push(Product product){
//如果容器满了,等待消费者消费
while(count>=products.length) {
//通知消费者消费,生产者等待
try {
System.out.println("========容器满了========");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有满,通知消费者消费
products[count]=product;
count++;
this.notifyAll();
}
//消费者消费产品
public synchronized Product pop(){
//判断能否消费
while(count==0){
//等待生产者生产
try{
System.out.println("========容器空了========");
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//可以消费
count--;
Product product=products[count];
this.notifyAll();
return product;
}
}
六、线程池
- JDK起提供了线程池相关APIExecutorService和Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):用来还行Runnable
//测试线程池
public class TestPool {
public static void main(String[] args) {
//1、创建线程池
//newFixedThreadPool,参数为线程池大小
ExecutorService service= Executors.newFixedThreadPool(10);
//执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//2、关闭链接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
结果
pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
pool-1-thread-2
pool-1-thread-6
pool-1-thread-5
pool-1-thread-7
pool-1-thread-8
pool-1-thread-9
pool-1-thread-9
pool-1-thread-10
pool-1-thread-9
无论执行多少个execute,都只会拿线程池里的线程
参考博文:狂神说java–多线程笔记(及源码)