文章目录
前言
在学多线程之前,需要将什么是程序
、进程
、线程
搞清楚
-
程序(program):是为了完成特定任务、用某种语言编写成的指令集合,即一段静态的代码
-
进程(process): 是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程。存在自身的产生、运行、消亡的过程,即一个生命周期,就像一个运行的浏览器。进程作为资源的分配单位,系统在运行的时候会为每一个进程分配不同的内存区域。
-
线程(thread):进程可以细分为为线程,是一个程序内部的一套执行路径。
-
一个进程如果支持多个线程同时运行,则就是支持多线程。
- 线程作为调度和执行的单位,每个线程都拥有独立的运行栈和程序计数器
-
其中一个
进程
中的多个线程
可以共享同一个内存单元/内存地址空间,即可以公用同一个对象。使用多线程主要的优点和缺点如下- 好处:使得线程之前通信更加简便、高效
- 坏处:可能会出现线程安全的问题。
-
生命周期
java程序的生命周期有5个状态:新建
、就绪
、运行
、阻塞
、死亡
线程
什么是多线程
多线程简单的来说就是在一个程序
中运行的多个线程
就比如电脑中有多个应用同时运行。这一个个程序就相当于一个个的进程
如何创建一个线程
在java中使用多线程有四种方式
- 继承Thread类
- 定义子类继承Thread类
- 子类重写Thread类中的
run()
方法,在我们调用子类对象的start()
就会自动调用run()
- 创建子类对象
- 调用线程对象(即子类对象)的
start()
启动线程,这个时候就会在主线程中运行一个副线程(即我们创建的线程对象)。
- 实现Runnable接口
- 定义子类,是按
Runnalbe接口
- 在子类中重写
run()
方法 - 利用Thread类的含参构造器构造子类,将实现
Runnable接口
的实现类对象作为参数传递到Thread类的含参构造器中 - 调用Thread类的
start()
方法启动线程.
- 定义子类,是按
- 实现Callable接口
- 与实现
Runnable
接口相似,不过它可以抛出异常,支持泛型,且在借助FutureTask
类下,run()
可以有返回值,并获取结果
- 与实现
- 使用线程池
虽然是有4种方式,但是都是需要重写父类/接口中的run()
方法.
继承Thread类
public class Test1 {
public static void main(String[] args) {
ThreadTest test = new ThreadTest();
test.start();//调用start()后,就会自动调用run()里面的内容
}
}
class ThreadTest extends Thread{
@Override
public void run() {
System.out.println("线程启动!");
}
}
实现Runnable接口
public class Test1 {
public static void main(String[] args) {
Thread test1 = new Thread(new ThreadTest());
test1.start();
}
}
class ThreadTest implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable接口");
}
}
实现Callable接口
在jdk5.0以后,新增了利用Callable接口
来完成线程的创建的方式。其中先来说一说为什么要加入这种方式来创建多线程。
在之前的两种创建线程的方式当中,都是通过重写run()
来完成线程的创建,但是run()是
无返回值的。而在新增的通过实现Callable接口
来创建线程的方式,是通过重写call()
,call()
与run()
方法具有一样的功能,相似的用法,但call()
可以有返回值
不过,通过实现Callable接口
方式来创建线程的方式还需要借助FutureTask类
,其中,该类实现了也Runnable接口
public class CallableTest {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumPrint numPrint = new NumPrint();
//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask<Integer> futureTask = new FutureTask(numPrint);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用statr()
new Thread(futureTask).start();
try {
//通过FutureTask类中的get()来获得call()的返回值
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//1.创建一个实现Callable的实现类
class NumPrint implements Callable<Integer> {
//2.实现Call方法,将此线程需要执行的操作声明在call()中
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum = sum + i;
System.out.println(i);
}
return sum;
}
}
线程池
jdk5.0以后,也新增了一个用线程池创建线程的方式
线程池顾名思义,就是一个可以容纳很多线程的池子,当我们创建线程的时候,就可以从线程池中取。
优点
- 提高响应速度(减少了创建线程的时间)
- 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
- 便于管理
线程池中主要的几个属性
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务是最多保持多长时间后会终止
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor serice1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
// serice1.setCorePoolSize(15);
// serice1.setKeepAliveTime(10);
//2.执行指定的线程的操作。需要提供实现Runnalbe接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合于用于Runnable
service.execute(new NumberThread1());//适合于用于Runnable
// service.submit(Callable callable);//适合使用与Callalbe
//3.关闭连接池
service.shutdown();
}
}
Thread中的方法
主要的使用的方法
方法 | 描述 |
---|---|
viod start() | 启动当前线程,并调用run() 方法 |
vod run() | 通常需要重写Thread类中的此方法,将要创建的执行操作声明在此方法之中 |
static Thread currentThread | 返回执行当前代码的线程 |
String getName() | 获得当前线程的名字 |
void setName() | 设置当前线程的名字 |
static void yield() | 释放当前CPU的执行权 |
void join() | 在线程a中调用线程b的join(),此时线程a就进入了阻塞状态,直到线程b完全执行后,线程a才会结束阻塞状态 |
void sleep(long millitime) | 让当前线程"睡眠"millitime毫秒,在当前millitime毫秒中,当前线程时阻塞状态的 |
boolean isAlive | 判断当前线程是否存活 |
int getPriority() | 返回此线程的状态 |
void setPriority(int P) | 设置线程的优先级 |
线程优先级
高优先级的线程会抢占低优先级cpu的执行权,但这只是从概率上来说,高优先级的线程高概率的情况下先被执行,并不意味这需要先执行完优先级的线程,才会执行低优先级的线程。
优先级最高有是MAX_PRIORITY:10
最低是MIN_PRIORITY:1
默认的优先级是:NORM_PRIORITY:5
public class Test1 extends Thread {
public static void main(String[] args) {
Thread test1 = new Thread(new ThreadTest());
test1.setName("线程一");
test1.start();//我这里没有设置它的优先级
}
}
class ThreadTest implements Runnable{
@Override
public void run() {
for(int i = 0;i<=100;i++){
if (i % 2 == 0)
System.out.println(Thread.currentThread().getName()+"\t" + i + " 优先级是:"+Thread.currentThread().getPriority());
}
}
}
多线程
多线程的创建
要实现多线程,就需要不同的线程有一个共享的资源
例如我们在网上抢票,多个窗口出售就那么一些票。这就可以看作是多线程。
其中通过实现Runnable接口的方式与继承Thread类的方式略有不同,不过效果都一样
通过实现Runnable接口
public class Test1{
public static void main(String[] args) {
ThreadTest test = new ThreadTest();//由于是实现Runnable接口来创建线程,我们就需要将总票数固定为100张,就需要创 建一个对象,然后将这对象作为参数传给Thread的构造器。
Thread window1 = new Thread(test);
Thread window2 = new Thread(test);
window1.setName("窗口1");
window2.setName("窗口2");
window1.start();
window2.start();
}
}
class ThreadTest implements Runnable{//这里实现Runnable接口
//总票数只有100张
int ticket = 100;
@Override
public void run() {
while(ticket>0) {
System.out.println(Thread.currentThread().getName() + "售出" + ticket + "张票");
ticket--;
}
}
}
继承Thread类
public class Test2 {
public static void main(String[] args) {
ThreadTest1 threadTest1 = new ThreadTest1();
ThreadTest1 threadTest2 = new ThreadTest1();
threadTest1.setName("1号窗口");
threadTest2.setName("2号窗口");
threadTest1.start();
threadTest2.start();
}
}
class ThreadTest1 extends Thread {
//总票数100
static int ticket = 100;//这里的票数需要声明为static,才能被多个线程共享同一个资源
@Override
public void run() {
while(ticket>0) {
System.out.println(Thread.currentThread().getName() + "售出" + ticket + "张票");
ticket--;
}
}
}
输出结果
在这里我们会发现有一些票重复出现,这就涉及到线程安全问题了
线程安全问题
怎么解决
出现这样的原因是因为我们cpu执行的速度很快,当我们cpu处理一个进程里面的一个线程的时候,这个线程的内容还没结束,另一个线程就进来了,就会造成上面的这个现象。
我们需要使用多线程的时候,尤其是这种情况的时候,需要使用到java的同步机制
当我们遇到这种问题的时候,可以在某一个线程访问共享资源的时候,给共享资源上一道锁(需要同一道锁),防止还有另外的线程来访问这个共享资源。这道锁叫做同步监听器
同步锁:
- 任何对象都可以作为同步锁。
- 同步方法的锁:静态方法(类名.class),非静态方法(this)
- 同步代码块的锁:随便,通常为this或类名.class
原来的具体的操作有三种方法
在jdk5.0以后,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
方法一:同步代码块
synchronized(同步锁){
需要被同步的代码
}
还是以上面买票的例子
public class Test1 {
public static void main(String[] args) {
ThreadTest test = new ThreadTest();
Thread window1 = new Thread(test);
Thread window2 = new Thread(test);
window1.setName("窗口1");
window2.setName("窗口2");
window1.start();
window2.start();
}
}
class ThreadTest implements Runnable {
int ticket = 10000;//如果设置100张票的话,因为cpu运行效率老快了,效果不明显
@Override
public void run() {
while (ticket > 0) {
synchronized (this) {//在这里添加。代码块,这里的同步锁是this,也就是ThreadTest的对象。这里保证了两个线程共用 了一个锁。
System.out.println(Thread.currentThread().getName() + "售出" + ticket + "张票");
ticket--;
}
}
}
}
//这里不可以将synchronized(){}移到while循环的外面,因为这样的话如果当线程1启动后,由于有同步锁的存在,其他线程就进入不了while循环,也就参与不了这个过程,就变成了全程线程1在执行了。
方法二:同步方法
public synchronized void show(String name){
需要被同步的代码
}
public class Test1 {
public static void main(String[] args) {
ThreadTest test = new ThreadTest();
Thread window1 = new Thread(test);
Thread window2 = new Thread(test);
window1.setName("窗口1");
window2.setName("窗口2");
window1.start();
window2.start();
}
}
class ThreadTest implements Runnable {
int ticket = 10000;
@Override
public void run() {
while (ticket > 0) {
sell();
}
}
public synchronized void sell(){
System.out.println(Thread.currentThread().getName() + "售出" + ticket + "张票");
ticket--;
}
}
我们需要把握synchronized的圈定范围,不能大了也不能小了。范围太小会没锁住所有的有安全问题的代码;范围太大会导致部分功能失效。
方法三:Lock锁
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
public class Test1 {
public static void main(String[] args) {
ThreadTest test1 = new ThreadTest();
ThreadTest test2 = new ThreadTest();
test1.setName("线程1");
test2.setName("线程2");
test1.start();
test2.start();
}
}
class ThreadTest extends Thread{
static int number = 100;
private final ReentrantLock lock = new ReentrantLock();
public void run(){
while(true){
lock.lock();
try {
if (number > 0){
System.out.println(currentThread().getName() + " " + number);
number--;
}else{
break;
}
}
finally {
lock.unlock();
}
}
}
}
死锁问题
死锁就是不同的线程分别占用对方所需要的资源但是不是释放cpu执行权,都在等待对方放弃自己所需要的同步资源 ,就形成了线程的死锁。
public class DeadLock {
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
Thread test1 = new Thread(thread1);
Thread test2 = new Thread(thread2);
test1.start();
test2.start();
}
}
//第一个线程Thread1
class Thread1 implements Runnable {
static StringBuffer str1 = new StringBuffer();
static StringBuffer str2 = new StringBuffer();
@Override
public void run() {
//这里先用str1作为锁,然后用str2作为锁
synchronized (str1) {
str1.append("a");
str2.append(1);
}
synchronized (str2) {
str1.append("b");
str2.append(2);
System.out.println(str1);
System.out.println(str2);
}
}
}
//第二个线程Thread2
class Thread2 implements Runnable {
//调用Thread1的str1与str2。
StringBuffer str2 = Thread1.str2;
StringBuffer str1 = Thread1.str1;
@Override
public void run() {
//这里先用str2作为锁,然后用str1作为锁,与线程Thread1相反
synchronized (str2) {
str1.append("c");
str2.append(3);
}
synchronized (str1) {
str1.append("d");
str2.append(4);
System.out.println(str1);
System.out.println(str2);
}
}
}
这样运行是不会出现死锁问题的,死锁是线程拿着锁不放,先看看结果
那么我就让线程里面进入第二个锁之前,都让他们睡着
public class DeadLock {
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
Thread test1 = new Thread(thread1);
Thread test2 = new Thread(thread2);
test1.start();
test2.start();
}
}
//第一个线程Thread1
class Thread1 implements Runnable {
static StringBuffer str1 = new StringBuffer();
static StringBuffer str2 = new StringBuffer();
@Override
public void run() {
//这里先用str1作为锁,然后用str2作为锁
synchronized (str1) {
str1.append("a");
str2.append(1);
}
//线程睡着了,sleep()是让线程睡着,但是不会释放锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (str2) {
str1.append("b");
str2.append(2);
System.out.println(str1);
System.out.println(str2);
}
}
}
//第二个线程Thread2
class Thread2 implements Runnable {
//调用Thread1的str1与str2。
StringBuffer str2 = Thread1.str2;
StringBuffer str1 = Thread1.str1;
@Override
public void run() {
//这里先用str2作为锁,然后用str1作为锁,与线程Thread1相反
synchronized (str2) {
str1.append("c");
str2.append(3);
}
//线程睡着了
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (str1) {
str1.append("d");
str2.append(4);
System.out.println(str1);
System.out.println(str2);
}
}
}
然后就发现什么都打印不出来,就说明都获取不到线程里面的第二个锁。