什么是线程??
线程是计算机执行的最小单位,在一个进程中可以有多个不同线程
多线程有什么用?干什么的?
单线程就像一个瓶子戳一个洞,而多线程是戳了多个洞,可以提高程序的执行效率,一个事情分配到不同线程同时执行,在写的程序往往会遇到同时处理多个问题的情况,而单线程必须上一个任务完成后才能执行下一个任务无法完成同时处理多个任务的情况,而多线程就是来解决这个问题的,不用等待上一个任务结束,提高了程序的响应度和效率.
第一步先打基础
一,Java 多线程基础
创建线程常用的有三种类型:
1,继承Thread类创建线程
2,实现Runnable接口创建线程
3,实现Callable接口创建线程
1,继承Thread类创建线程
``
public class demo {
public static void main(String[] args) {
//实例化我们自定义的线程类
MyThread myThread1=new MyThread("A线程");
MyThread myThread2=new MyThread("B线程");
//start开启我们的线程
myThread1.start();
myThread2.start();
}
}
class MyThread extends Thread{
//继承Thread之后要实现run方法,run里面就是要实现多线程的方法
String name;
MyThread(String name){
this.name=name;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Thread.currentThread().setName(name);
//Thread.currentThread().getName()获取本线程的名字,当然名字也可以修改,自己也有默认值
System.out.println(Thread.currentThread().getName()+":正在执行"+i);
}
}
}
执行结果:
B线程:正在执行0
A线程:正在执行0
A线程:正在执行1
B线程:正在执行1
A线程:正在执行2
B线程:正在执行2
A线程:正在执行3
A线程:正在执行4
A线程:正在执行5
A线程:正在执行6
....................
这样我们可以看出实现了多线程,两个循环输出的结果交叉输出,但是也看出一个问题.
为啥这俩为什么不是一替一次的交叉运行???
1,因为他俩运行需要cpu进行分配时间片,每个线程给他分配一个时间段去处理数据,然后下一个时间段就可能处理其他线程了,cup在线程里来回切换,只要速度够快就可以实现多线程的效果,
2,因为cpu去选择线程是随机的,所以不一定一替一次交叉运行,只能说下一次他俩执行的概率一样
3,因为cpu的性能比较强任务量相对比较小,每个时间片可能足够执行此循环多次,导致获得一次执行机会就可以输出多次结果
综合上面的原因导致了,输出结果并不是一替一次交叉输出.
CPU分时间片交替执行,宏观并行,微观串行,如今的CPU已经发展到了多核CPU,真正存在并行。
2.实现Runnable
接口创建线程
Runnable和Thread有什么差异??
Runnable是一个接口可以在实现run方法开启多线程的同时还能去继承其他类,而Thread是继承而来由于Java只能单继承如果继承了Thread就无法同时直接继承其他类,灵活度相比比较低,在开发中Runnable用的较多,Runnable实例要交给Thread去执行,从字面上也可以理解,Runnable是一个可执行的任务,任务要交给线程去管理.
``
public class demo {
public static void main(String[] args) {
//把Runnable实例化后交给Thread去执行
Thread thread1=new Thread(new MyRunnable());
Thread thread2=new Thread(new MyRunnable());
//执行线程
thread1.start();
thread2.start();
}
}
//由于Runnable是接口,解决了Thread的单继承的问题
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"正在执行"+i);
}
}
}
执行结果:
Thread-0正在执行0
Thread-1正在执行0
Thread-0正在执行1
Thread-1正在执行1
Thread-0正在执行2
Thread-1正在执行2
Thread-0正在执行3
Thread-1正在执行3
Thread-1正在执行4
Thread-1正在执行5
Thread-1正在执行6
Thread-0正在执行4
.....................
3,使用Callable接口创建线程
Callable和Runnable的用法类似,但是Callable具有返回值,可以获得线程的返回值,使用Future接口来接收返回值,要先用Callable实现Future接口再把实例传给Thread去执行,然后使用Future的get方法获得返回结果
``
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable callable1=new MyCallable("A");
Callable callable2=new MyCallable("B");
Callable callable3=new MyCallable("C");
//实现Future接口
FutureTask<Object> f1=new FutureTask<>(callable1);
FutureTask<Object> f2=new FutureTask<>(callable2);
FutureTask<Object> f3=new FutureTask<>(callable3);
Thread t1=new Thread(f1);
Thread t2=new Thread(f2);
Thread t3=new Thread(f3);
//在callable实现后如果不去启动线程,主线程会一直去等待callable返回结果,返回结果后才会继续执行下一步
//如果我们把下面的t2.start()注释线程t3则不会执行,因为主线程在等待t2结束
t1.start();
t2.start();
t3.start();
//获得返回值
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
}
}
class MyCallable implements Callable {
String name;
MyCallable(String name){
this.name=name;
}
@Override
public Object call() throws Exception {
return name+"线程返回值";
}
}
执行结果:
A线程返回值
B线程返回值
C线程返回值
在callable实现后如果不去启动线程,主线程会一直去等待callable返回结果,返回结果后才会继续执行下一步,如果t2注释线程t3则不会执行,因为主线程在等待t2结束
当然也可以使用线程池,使用线程池也可以创建Callable,线程池会在下一篇文章中进行详细介绍.
``
import java.util.concurrent.*;
import java.util.concurrent.Callable;
public class demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程池
ExecutorService service= Executors.newFixedThreadPool(2);
//实例化Callable
Callable aa = new MyCallable("aa");
Callable bb = new MyCallable("bb");
Callable cc = new MyCallable("cc");
//实现Future接口
Future f1= service.submit(aa);
Future f2= service.submit(bb);
Future f3= service.submit(cc);
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
service.shutdown();
}
}
class MyCallable implements Callable {
private String string;
MyCallable(String string){
this.string=string;
}
@Override
public Object call() throws Exception {
return string+"返回值";
}
}
4,其他创建线程的方法
这些方法只是用到接口的特点并不是线程特有的,原理和上面的相同只是语法不同,
这些是JDK的特征,使用匿名内部类和lambda表达式
``
public class demo {
public static void main(String[] args) {
//使用匿名内部类
Thread thread1 =new Thread(new Runnable() {
@Override
public void run() {
System.out.println("创建了线程a");
}
});
//使用Lambda表达式
Thread thread2=new Thread(()->{
System.out.println("创建了线程b");
});
//开启线程
thread1.start();
thread2.start();
}
}
二,线程的同步,什么是线程同步??
线程的同步是保证多线程安全访问竞争资源的一种手段。
同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。
为什么要同步?同步有什么用??
在多个线程对数据进行读写时就需要对数据进行同步,多个线程同时读写数据难免会出错.
我们在使用多线程的时候往往是多个线程一起去做一件事,多个线程对一个数据进行操作,但是这个数据如果大家一起挤进去做难免会出现错误,因为可能一个线程正在做一半,然后就切换到另一个线程去做这个事,但是上一个线程的东西还没有处理完数据没有来得及保存,下一个线程瞎搞就可能出现数据异常.
使用synchronized进行同步,就是synchronized方法里面有线程的话,其他线程就去等待这个线程去处理完,其他线程再进入synchronized方法里进行数据处理,synchronized里面始终保持一个线程在运行,就不用担心线程合作不默契导致的数据错误.
synchronized:是一种同步方法,它需要传入参数去确定同步的是同一个同步代码块或方法,synchronized方法的同步锁是this,而synchronized代码块需要传入一个Object去锁定代码块,synchronized判断锁是否为同一个时通过判断object对象进行的,如果new 的object对象在对象里面就会导致锁不是同一把锁,因此我们通常使用静态变量去命名锁,使得多个实例化对象用的是同一把锁,防止因为锁来自于不同对象而导致数据的异步
使用同步方法的方式来进行同步:在需要同步的方法的前面加上synchronized可以实现线程的同步.
``
public class sellTicketDemo {
public static void main(String[] args) {
多线程.线程同步.sellTicket st =new sellTicket();
Thread t1=new Thread(st);
Thread t2=new Thread(st);
Thread t3=new Thread(st);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class sellTicket implements Runnable{
private int tickets=100;
private Object obj =new Object();
@Override
public void run() {
synchronized (this) {
while (true) {
sellTicket();
}
}
}
private synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出第:" + tickets + "张票");
tickets--;
}
}
}
线程的死锁问题
死锁问题对于java来说是非常复杂的,问题也比较难发现
因为死锁是偶然事件,当我们重复操作时可能就无法复现死锁的情况,为我们寻找bug增加了难度,但是这并不意味着我们不需要关心死锁,死锁一旦出现我们没有相关处理机制,程序就会崩溃只能重启.
死锁原因:主要是线程的相互等待资源导致不能进一步运行
例子:
两个人一起吃饭但是只有一双筷子,一个人吃完后释放资源把筷子放到桌子上,然后两个人竞争资源谁抢到谁吃饭,但是如果出现a抢到一个资源的时候也就是一根筷子的时候,另一根被b抢走了,a在等b的筷子,b在等a的筷子,两个人互相等待都吃不上饭,这样就进入了死锁状态
死锁主要就是线程间的相互等待资源引起的
三,线程的调度和通信
优先级
从上面我们知道,线程是由cpu去调度的,它随机选择一个线程去分配时间片去执行,而线程的优先级就是改变线程的随机调度的概率,概率不同优先级就不同了,线程总共有10个优先级优先级越大被分配时间片的概率也就越大.
``
public class TreadPriorityDemo {
public static void main(String[] args) {
ThreadPriority td1=new ThreadPriority();
ThreadPriority td2=new ThreadPriority();
ThreadPriority td3=new ThreadPriority();
td1.setName("高铁");
td2.setName("飞机");
td3.setName("汽车");
//线程优先级最大范围是10 最小范围是1
//线程优先级高仅仅是获取cpu时间片的几率高
td1.setPriority(1);
td2.setPriority(2);
td3.setPriority(3);
td1.start();
td2.start();
td3.start();
}
}
public class ThreadPriority extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
高铁:0
高铁:1
高铁:2
高铁:3
高铁:4
高铁:5
高铁:6
汽车:0
汽车:1
汽车:2
飞机:0
汽车:3
使用 td1.setPriority(1);方法去设置优先级1-10,线程优先级高仅仅是获取cpu时间片的几率高而不是必定优先执行
休眠
线程休眠的目的就是让出cpu,对是否执行进行严格的控制.线程休眠的方法就是Thread.sleep()均为静态方法
public class cc {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(1000);
}
}
这样主线程就休眠了,想要那个线程休眠就调用那个线程的sleep
等待
wait()方法 是属于object包下的一个方法是导致当前的线程进入等待状态直到有其他线程对他进行 唤醒
wait(long milis) 导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者超过指定的时间量。
wait(long timeout,int nanos) 导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
唤醒
notfiy() 唤醒在此监视器上的等待的单个线程随机唤醒一个
notfiyAll() 唤醒在此监视器上的等待的全部线程
public class demo {
public static void main(String[] args) {
Calculator calculator = new Calculator();
ReaderResult readerResult1 = new ReaderResult(calculator);
ReaderResult readerResult2 = new ReaderResult(calculator);
ReaderResult readerResult3 = new ReaderResult(calculator);
readerResult1.start();
readerResult2.start();
readerResult3.start();
calculator.start();
}
}
public class ReaderResult extends Thread {
Calculator c;
ReaderResult(Calculator c){
this.c=c;
}
@Override
public void run() {
synchronized(c){
try {
System.out.println(this.getName()+"等待结果");
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("等待结果为:"+c.total);
}
}
}
public class Calculator extends Thread{
int total=0;
@Override
public synchronized void run() {
for (int i = 0; i < 100; i++) {
total+=i;
try {
wait(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
notifyAll();
}
}
Thread-1等待结果 Thread-3等待结果 Thread-2等待结果 等待结果为:4950 等待结果为:4950 等待结果为:4950
让三个线程先进入等待,然后再启动一个线程两秒后全部唤醒
让步
让步含义就是让出cpu资源,但是让出给谁不知道,只是让出,线程恢复到可执行状态,让出后下一次执行可能还是本线程,让步就是结束本线程变成就绪状态,然后大家再次一起去抢夺cpu资源,刚刚的实例可能还会抢到线程的执行权
线程让步的方法 Tread.yield(),功能是暂停本线程变为可执行状态
public static void main(String[] args) {
MyThread my1=new MyThread();
MyThread my2=new MyThread();
my1.setName("aa");
my2.setName("bb");
my1.start();
my2.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Thread.yield();
System.out.println(getName()+":"+i);
}
}
}
bb:0
aa:0
bb:1
aa:1
bb:2
aa:2
bb:3
bb:4
aa:3
bb:5
aa:4
bb:6
aa:5
bb:7
可以看出虽然只有两个线程,但是出现的连续的bb,可以看出bb在让步之后下一次又抢到了执行权才出现bb的情况
线程的合并
线程的合并就是将几个线程合并为一个单线程执行,应用场景是一个线程要等待另一个线程结束后才能执行可以使用join方法
join()等待该线程终止
join(long millis 等待线程终止的最长的毫秒数)
join(long millis,int nanos) 等待该线程终止的时间最长为millis毫秒,nanos纳秒
public class gateNmae {
public static void main(String[] args) {
MyThread my1=new MyThread();
my1.setName("aa");
my1.start();
}
}
class MyThread extends Thread{
MyThread2 myThread2=new MyThread2();
@Override
public void run() {
myThread2.start();
for (int i = 0; i < 10; i++) {
Thread.yield();
System.out.println(getName()+":"+i);
try {
myThread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("插入线程");
}
}
}
aa:0
插入线程
插入线程
插入线程
插入线程
插入线程
aa:1
aa:2
aa:3
aa:4
aa:5
aa:6
aa:7
aa:8
aa:9
可以看出在aa执行第一次时我们加入使用join线程当join的线程结束之后,才继续执行此线程
守护线程
调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。
public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。
该方法必须在启动线程前调用。
public class MyRunnableDemo {
public static void main(String[] args) {
MyRunnable my1 =new MyRunnable();
Thread t2 =new Thread(my1,"飞机");
t2.setDaemon(true);
t2.start();
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
0
1
2
3
4
5
6
7
8
9
飞机:0
飞机:1
飞机:2
飞机:3
飞机:4
飞机:5
飞机:6
飞机:7
飞机:8
飞机:9
飞机:10
飞机:11
当主线程结束后,守护线程也会一同结束,setdeamon是把程序变为后台执行,如果前台执行完毕后台会一同退出
实际上:JRE判断是否执行结束的标准是所有前台程序执行完毕了,而不管后台程序,只要前台执行完毕后台一同结束
这一篇主要介绍线程的语法以及一些创建方法
下一篇 主要介绍线程的同步问题以及线程锁的相关问题