目录
6.2.3 synchronized和ReentrantLock的区别
6.3.1 synchronized关键字(继承Thread类)
6.3.2 synchronized关键字(实现Runnable接口)
6.3.3 ReentrantLock类(继承Thread类)
6.3.4 ReentrantLock类(实现Runnable接口)
一. 程序 进程 线程的概念
程序: 为了实现某种功能,通过编程语言写的一系列的指令的集合.
指的是在硬盘上存储的静态代码
进程: 被操作系统调度到内存中执行的程序(正在执行的程序),是操作系统进行资源分配的最小单位
线程: 进程可以进一步细化为线程,是进程中的最小执行单元(任务),是cpu调度的最小单位
1.1 进程和线程的关系
1.一个进程中可以包含多个线程(一个QQ可以有多个聊天窗口)
2.一个线程只能隶属于一个进程(QQ聊天窗口只能隶属于QQ进程)
3.一个进程中至少包含一个线程(主线程,java的main方法就是用来启动主线程的)
4.在主线程中可以创建并启动其他线程
5.一个进程的所有线程共享该进程的所有资源
二. 线程的创建
2.1 继承Thread类
要想在java程序中创建一个线程,第一种方式是继承Thread类,实现其中的run()方法,将线程中想要执行的任务写在run方法中,再调用Thread中的start()方法
注意:一定不要调用run()方法,如果调用该方法,那么并没有创建一个线程,该程序中还是main一个主线程,代码的执行逻辑仍然是从上向下执行,只有调用了start()方法才是真正创建了一个独立的线程
public class MyThread extends Thread{
/*
java中创建线程方式1
写一个类继承java.lang.Thread
重写run()
*/
/*
线程中要执行的任务都要写在run()中,或者在run()中调用
*/
@Override
public void run() {
/*for (int i = 0; i < 1000; i++) {
System.out.println("MyThread:"+i);
}*/
test();
}
public void test()
{
for (int i = 0; i < 1000; i++) {
System.out.println("MyThread:"+i);
}
}
}
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);
}
}
}
2.2 实现Runnable接口
定义一个类实现Runnable接口,实现里面的run()方法,此时只是创建了一个线程要执行的任务,然后再创建Thread类的对象,通过构造方法传入自定义类的对象即可完成线程的创建
public class Task implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("自定义线程:"+i);
}
}
/*
java中创建线程方式2:
只先创建要执行的任务,创建一个类,实现Runnable接口
重写任务执行的run()
实现Runnable接口创建的优点:
1.因为java是单继承,一旦继承一个类就不能在继承其他类,避免单继承的局限
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 < 1000; i++) {
System.out.println("main:"+i);
}
}
}
2.3 实现Callable接口
实现Callable接口创建线程是一种比较强大的线程创建方式,相比于上面两种方式,该方式可以有返回值,也可以抛异常,实现该接口,并实现里面的call()方法
public class SumTask<T> implements Callable<T> {
@Override
public T call() throws Exception {
Integer sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
return (T)sum;
}
}
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
SumTask<Integer> sumTask = new SumTask<>();
FutureTask<Integer> futureTask = new FutureTask<>(sumTask);
Thread thread = new Thread(futureTask);
thread.start();
Integer sum = futureTask.get();//获取call方法返回的执行结果
System.out.println(sum);
}
}
注意:
1.由于Thread的构造方法只允许传入一个实现了Runnable接口的方法,所以我们要利用一个FutureTask类进行转换(将Callable转化为Runnable),该类的构造方法允许传入一个Callable接口,并且该类实现了RunnableFuture接口,而RunnableFuture接口又继承了Runnable接口和Future接口,通过多态性可将Callable转为Runnable
2.可以通过FutureTask类中的get()方法获得call()方法的返回值
三. Thread类常用方法
run() | 用来定义线程要执行的任务代码 |
start() | 启动线程的 |
currentThread() | 获取到当前线程 |
getId() | 获取线程id |
setName() | 为线程设置名字 |
getState() | 获取线程的状态 |
getPriority() | 获取线程的优先级 |
setPriority(10) | 设置线程优先级 优先级为1-10 默认是5 作用是为操作系统调度算法提供的 |
getName() | 获取线程名字 |
join() | 等待调用了join()方法的线程执行完毕,其他线程再执行 |
yield() | 主动礼让,让出cpu执行权 |
sleep(long millis) | 让进程睡眠阻塞一定时间,参数是毫秒 |
四. 线程的状态
新建:刚创建一个线程对象,没有调用start()方法启动该线程
就绪(可运行状态):调用start()方法后,线程就进入了就绪状态,在就绪队列中等待cpu调度
运行:被操作系统调度到cpu上执行
阻塞:进程因没有获得相应的资源而处于等待,例如调用了sleep(), 有线程调用了join(),线程中进行Scanner输入
死亡/销毁:进程的run()方法执行完等,使进程被销毁
五. 多线程
多线程:在同一个程序中创建了多个线程执行
5.1 多线程优缺点
优点:
1. 提高了cpu的利用率
2. 提高程序运行的效率
3. 改善程序结构, 例如将一个大的任务拆分成若干个小任务执行
缺点:
1. 线程过多,增加了内存开销
2. cpu开销大
3. 当多个线程对同一份共享资源进行访问时,如果不加以控制,就会出现严重的错误,比如:电商购物,买票等
六. 解决多线程访问共享资源问题
6.1 卖票案例
public class TicketThread extends Thread{
static int num = 100000;
@Override
public void run() {
while(true){
if(num > 0){
System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
num--;
}else{
break;
}
}
}
}
public class TicketTest {
public static void main(String[] args) {
TicketThread thread1 = new TicketThread();
thread1.setName("窗口一");
thread1.start();
TicketThread thread2 = new TicketThread();
thread2.setName("窗口二");
thread2.start();
}
}
由于窗口一和窗口二两个线程对同一个资源num进行访问,且没有加以控制就会导致出票错误,有可能重票,有可能错票,这在实际生活中是很严重的错误
可以看到窗口一和窗口二同时买到了第10张票.
6.2 线程同步
解决该问题的方法就是线程同步,即在对共享资源进行访问时加一把锁,一次只允许一个进程访问,等这个进程访问完,其他进程才可以访问该共享资源
6.2.1 synchronized关键字
上述说的加锁就是在对进程同时访问的代码块中加synchronized关键字,即可完成加锁
6.2.1.1 synchronized修饰代码块
synchronized(同步锁对象){
同步代码块
}
/*
同步锁对象作用:用来记录有没有线程进入到同步代码块中,如果有线程进入到同步代码块,那么其他线程就不能进入同步代码块
直到上一个线程执行完同步代码块的内容,其他线程才能进入
同步锁对象的要求:可以是任意类的对象
同步锁对象必须是唯一的(多个线程拿到的是同一个对象)
*/
同步锁对象作用:用来记录有没有线程进入到同步代码块中,如果有线程进入到同步代码块,那么其他线程不能进入同步代码块中
同步锁对象的要求:可以是任意类的对象,同步锁对象必须是唯一的(多个线程拿到的是同一个对象)
比如:被static修饰的就是同一个对象
6.2.1.2 synchronized修饰方法
在方法前面加上synchronized关键字
public class TicketTask extends Thread{
static int num = 10;//模拟有10张票
static Object obj = new Object();
/*
synchronized修饰方法时,同步锁对象不需要我们指定
同步锁对象会默认提供:
1.非静态的方法--锁对象默认是this
2.静态方法--锁对象是当前类的Class对象(类的对象,一个类的对象只有一个)
*/
public static synchronized void Print()
{
if(num>0){
System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
num--;
}
}
@Override
public void run() {
while(true)
{
if(num <= 0)
{
break;
}
Print();
}
}
}
synchronized修饰方法时,同步锁对象不用我们指定,会默认生成
1.非静态方法:锁对象默认是this,当前对象
2.静态方法:锁对象默认是当前类的Class对象(类的对象,一个类的对象只有一个)
6.2.2 ReentrantLock类
ReentrantLock类中的lock()方法和unlock()方法也可以实现,synchronized关键字的作用,只不过需要注意的是,使用这两个方法时需要我们手动的加锁和释放锁,而且也要注意两个不同的对象要用同一个ReentrantLock对象
public class TicketThread extends Thread{
static int num = 10;
static ReentrantLock lock = new ReentrantLock();//实现锁控制的对象
@Override
public void run() {
while (true){
try{
lock.lock();//0-1 加锁
if(num>0){
System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
num--;
}else{
break;
}
}finally {
lock.unlock();//1-0 释放锁
}
}
}
/*
synchronized 和 ReentrantLock区别:
synchronized是一个关键字,控制依靠底层编译后的指令去实现
synchronized可以修饰一个方法,还可以修饰一个代码块
synchronized是隐式的加锁和释放锁,一旦方法或代码块中运行结束或出现异常,会自动释放锁
ReentrantLock是一个类,是依靠java代码去控制(底层有一个同步队列)
ReentrantLock只能修饰代码块
ReentrantLock需要手动的加锁,手动的释放锁,所以释放锁最好写在finally中,一旦出现异常,保证锁能释放
*/
}
6.2.3 synchronized和ReentrantLock的区别
1.synchronized是一个关键字,底层依靠编译后的指令实现,ReentrantLock是一个类内部依靠java代码实现(底层有一个同步队列)
2.synchronized可以修饰一个代码块,也可以修饰一个方法,,ReentrantLock只能修饰代码块
3.synchronized是隐式的加锁释放锁,一旦方法或代码块中运行结束或出现异常,会自动释放锁
ReentrantLock需要手动的加锁,手动的释放锁,所以释放锁最好写在finally中,一旦出现异常,保证能释放锁
6.3 利用线程同步解决卖票案例
6.3.1 synchronized关键字(继承Thread类)
synchronized修饰代码块
public class TicketThread extends Thread{
static int num = 10;
static Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj)
{
if(num > 0){
System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
num--;
}else{
break;
}
}
}
}
}
synchronized修饰方法
public class TicketThread extends Thread{
static int num = 1000;
public static synchronized void print()
{
if(num > 0)
{
System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
num--;
}
}
@Override
public void run() {
while(true)
{
if(num <= 0)
{
break;
}
print();
}
}
}
6.3.2 synchronized关键字(实现Runnable接口)
synchronized修饰代码块
public class TicketTask implements Runnable{
int num = 10;//模拟有10张票
@Override
public void run() {
while (true){
synchronized (this){// obj对象的作用:记录有没有线程进入到同步代码块 要求:多个线程对应同一个同步锁对象
if(num>0){
System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
num--;
}else{
break;
}
}
}
}
}
synchronized修饰方法
public class TicketTask implements Runnable{
int num = 10;//模拟有10张票
public synchronized void Print()
{
if(num>0){
System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
num--;
}
}
@Override
public void run() {
while(true)
{
if(num <= 0)
{
break;
}
Print();
}
}
}
6.3.3 ReentrantLock类(继承Thread类)
public class TicketThread extends Thread{
static int num = 10;
static ReentrantLock lock = new ReentrantLock();//实现锁控制的对象
@Override
public void run() {
while (true){
try{
lock.lock();//0-1 加锁
if(num>0){
System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
num--;
}else{
break;
}
}finally {
lock.unlock();//1-0 释放锁
}
}
}
}
6.3.4 ReentrantLock类(实现Runnable接口)
修饰代码块
public class TicketTask implements Runnable{
ReentrantLock lock = new ReentrantLock();
int num = 1000;
@Override
public void run() {
while(true)
{
try {
lock.lock();
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "买到了第" + num + "张票");
num--;
}else{
break;
}
}finally {
lock.unlock();
}
}
}
}
七. 线程通信
Objtct类中有wait()方法,notify()方法,notifyAll()方法,可以用来实现线程间的通信
wait()方法 | 使进入同步代码块中的方法进入等待状态 |
notify()方法 | 唤醒上一个在等待状态的线程 |
notifyAll()方法 | 唤醒所有在等待状态的线程 |
注意:
这三个方法都必须在同步代码块中调用,并且必须通过同步锁对象调用
wait()和sleep()的区别
1. sleep()方法属于Thread类的方法,wait()方法属于Object类的方法
2.sleep()方法睡眠时间到后,会被自动唤醒,wait()方法,需要被notify()或notifyAll()方法唤醒
3.sleep()方法不会释放锁对象,wait()方法会释放锁对象
7.1 案例一(两个线程交替打印数字)
public class PrintNumThread extends Thread{
static int num = 1;
static Object obj = new Object();
@Override
public void run() {
while(num<=100) {
synchronized (obj) {
obj.notify();
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
try {
obj.wait();//让线程等待,会自动释放锁,notify(),wait()必须都在同步代码块(同步方法)中通过同步锁对象调用
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
obj.notify();
}
}
}
}
}
public class Test {
public static void main(String[] args) {
PrintNumThread thread1 = new PrintNumThread();
thread1.setName("线程1");
thread1.start();
PrintNumThread thread2 = new PrintNumThread();
thread2.setName("线程2");
thread2.start();
}
}
7.2 案例二(生产者消费者模型)
public class ProductorThread extends Thread{
Counter counter;
public ProductorThread(Counter counter)
{
this.counter = counter;
}
@Override
public void run() {
while(true)
{
try {
counter.add();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
生产者线程,用来生产产品
public class CustomerThread extends Thread{
Counter counter;
public CustomerThread(Counter counter)
{
this.counter = counter;
}
@Override
public void run() {
while(true)
{
try {
counter.sub();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
消费者线程,用来消费产品
public class Counter {
int num = 0;//柜台可以存放的商品数量
//生产者调用 添加商品
public synchronized void add() throws InterruptedException {
if(num <= 0)
{
this.notify();
num = 1;
System.out.println("生产者生产了一件商品");
}else{
this.wait();
}
}
//消费者调用 取走商品
public synchronized void sub() throws InterruptedException {
if(num>0)
{
this.notify();
num = 0;
System.out.println("消费者消费了一件商品");
}else{
this.wait();
}
}
}
柜台类,用于表示柜台上有多少个产品和增加产品消费产品的方法,分别给生产者和消费者调用
public class Test {
public static void main(String[] args) {
Counter counter = new Counter();
ProductorThread productorThread = new ProductorThread(counter);
productorThread.setName("生产者");
productorThread.start();
CustomerThread customerThread = new CustomerThread(counter);
customerThread.setName("消费者");
customerThread.start();
}
}
测试类,创建生产者消费者线程,一边生产一边消费