一、线程简介
- 多任务:同一时间有多个任务要去做
- 多线程:原来只有一条路,慢慢的因为车太多了,道路阻塞,效率极低,为了提高使用的效率,能够充分利用道路,于是加了多个车道。从此,麻麻再也不用担心道路阻塞了!
- 普通方法调用和多线程
进程与线程
线程就是独立的执行路径
在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc(垃圾回收)线程
main()称之为主线程,为系统的入口,用于执行整个程序
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
线程会带来额外的开销,如cpu调度时间,并发控制开销(让线程排队执行)
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
线程实现
线程创建
线程的创建有三种方式:
- 继承Thread
- 实现Runnable
- 实现Callable
main函数实自己写的,叫用户线程
gc线程是JVM给的,叫守护线程
继承Thread类(重点)
不建议使用:为了避免OOP单继承局限性
Thread类本身实现了Runnadle接口
- 自定义线程类继承Thread类
- 重写run方法,编写程序执行体
- 创建线程对象,调用start()方法启动线程
总结:线程开启不一定立即执行,由CPU调度执行,直接调用run方法相当于调用普通方法,不会创建新的线程
public class TestThread1 extends Thread {//继承Thread类
public static void main(String[] args) {
TestThread1 thread1=new TestThread1();
// thread1.run();//先执行run方法,再执行接下来的代码(没起到多线程的作用)
thread1.start();//主线程和子线程交替执行,且不可控,每次执行结果都不一样
for (int i = 0; i < 20; i++) {
System.out.println("主线程"+i);
}
}
@Override
public void run() {
// super.run();
for (int i = 0; i < 20; i++) {
System.out.println("子线程"+i);
}
}
}
案例多线程下载图片
需要用到commons-io2.6这个jar包,因为我们需要用到commons-io包里面的FileUtils函数
然后把包拷贝到我们的工程下面,右键 Add as library
package MultiThread;
import org.apache.commons.io.FileUtils;
import sun.misc.FileURLMapper;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
//实现多线程同步下载网图
public class TestThread2 extends Thread{
private String url;//网络图片地址
private String name;//保存的文件名
@Override
public void run()
{
WebDownLoader webDownLoader=new WebDownLoader();
webDownLoader.downloader(url,name);
System.out.println("下载了文件名为"+name);
}
public TestThread2(String url, String name) {
this.url = url;
this.name = name;
}
public static void main(String[] args) {
TestThread2 t1=new TestThread2("https://image.baidu.com/search/detail?ct=503316480&z=0&ipn=d&word=%E5%B0%8F%E7%8B%97%E5%9B%BE%E7%89%87&hs=2&pn=2&spn=0&di=68640&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=3788223672%2C2933772794&os=3469738239%2C2819470525&simid=0%2C0&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=&bdtype=0&oriquery=%E5%B0%8F%E7%8B%97%E5%9B%BE%E7%89%87&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20191105%2F15%2F1572937434-vjDJNuBAxl.jpg%26refer%3Dhttp%3A%2F%2Fimage.biaobaiju.com%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Djpeg%3Fsec%3D1613979841%26t%3D6b74022d98f53152b4e7143934ed2714&fromurl=ippr_z2C%24qAzdH3FAzdH3Fooo_z%26e3Bktw5kwt37_z%26e3Bv54AzdH3Fi5g2geiwtztAzdH3Fl08nc_z%26e3Bip4s&gsm=3&islist=&querylist=","dog.jpeg");
TestThread2 t2=new TestThread2("https://image.baidu.com/search/detail?ct=503316480&z=0&ipn=d&word=%E5%B0%8F%E7%8B%97%E5%9B%BE%E7%89%87&hs=2&pn=5&spn=0&di=26840&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=2057173425%2C3288346039&os=3534193242%2C1744020036&simid=4255926711%2C726475710&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=&bdtype=0&oriquery=%E5%B0%8F%E7%8B%97%E5%9B%BE%E7%89%87&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%3A%2F%2Fimg.wohaoyun.com%2Fimg_600%2FM00%2F07%2FB8%2FwKjg2lvW-dGAERdjAAEeYGzmpUA906.jpg%26refer%3Dhttp%3A%2F%2Fimg.wohaoyun.com%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Djpeg%3Fsec%3D1613979935%26t%3Dbd8042b4b6488deae4c73854e0c4dd6e&fromurl=ippr_z2C%24qAzdH3FAzdH3Fooo_z%26e3Bo5iw5y7g_z%26e3Bv54AzdH3Ff3AzdH3F15g2o7ktzitAzdH3Fda8bAzdH3F88AzdH3F8988_z%26e3Bip4s&gsm=6&islist=&querylist=","dog2.jpeg");
TestThread2 t3=new TestThread2("https://image.baidu.com/search/detail?ct=503316480&z=0&ipn=d&word=%E5%B0%8F%E7%8B%97%E5%9B%BE%E7%89%87&hs=2&pn=4&spn=0&di=13750&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=2136279165%2C2845293579&os=1684394320%2C374773828&simid=3423840844%2C312231934&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=&bdtype=0&oriquery=%E5%B0%8F%E7%8B%97%E5%9B%BE%E7%89%87&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%3A%2F%2Fwww.cpnic.com%2FUploadFiles%2Fimg_0_3308088708_3867205912_26.jpg%26refer%3Dhttp%3A%2F%2Fwww.cpnic.com%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Djpeg%3Fsec%3D1613980066%26t%3D73944a43d2a1181ed0e4aeb6cd59da1e&fromurl=ippr_z2C%24qAzdH3FAzdH3Fooo_z%26e3Bvrgtv_z%26e3Bv54AzdH3FrtvAzdH3F%25Ec%25Ba%25bF%25Ec%25lE%25bB%25E0%25bB%25l0%25Ec%25ln%25b8%25E0%25A0%25bD%25Ec%25A9%25A0%25Ec%25bc%25Ab%25Ec%25bF%25bA%25Ec%25lB%25BE%25E0%25bl%25b0%25Ec%25bF%25bA%25E9%25BB%25B0%25Em%25Aa%25BCAzdH3F&gsm=5&islist=&querylist=","dog3.jpeg");
t1.start();
t2.start();
t3.start();
}
}
//下载器
class WebDownLoader{
//下载方法
public void downloader(String url,String name) {
try {
FileUtils.copyURLToFile(new URL(url),new File(name));//copyURLToFile,把url变成文件
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}
实现Runnable接口(重点)
Java是单继承,推荐使用Runnable接口,方便同一个独享被多个线程使用
避免了单继承的局限性:即在Java中一个类只能使用extends继承一个父类,如果继承多个父类,而父类有同名方法时就不知道调用哪一个方法了,另外回事两个类的耦合性增加,如果父类有改动时会直接影响子类。
- 定义MyRunnable类实现Runnable接口
- 实现Run()方法,编写程序执行体
- 创建线程对象,调用start()方法启动线程
package MultiThread;
public class TestThread3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("子线程"+i);
}
}
public static void main(String[] args) {
//创建Runnable接口实现类的对象
TestThread3 testThread3 = new TestThread3();
//创建线程类对象,通过线程对象开启我们的线程(代理)
// Thread thread=new Thread(testThread3);
// thread.start();
new Thread(testThread3).start();
for (int i = 0; i < 20; i++) {
System.out.println("主线程"+i);
}
}
}
初始并发问题
多个线程操作同一个资源的情况下出现不同的线程抢到同一张票,线程不安全,数据会紊乱
package MultiThread;
//多个线程操作同一个对象
//买火车票
//多个线程操作同一个资源的情况下出现不同的线程抢到同一张票,线程不安全,数据紊乱
public class TestTheread4 implements Runnable{
int ticketNums=100;//票数
public static void main(String[] args) {
TestTheread4 testTheread4 = new TestTheread4();
new Thread(testTheread4,"1").start();//1,2,3是线程名字
new Thread(testTheread4,"2").start();
new Thread(testTheread4,"3").start();
}
@Override
public void run() {
while (true)
{
if(ticketNums<=0)
{
break;
}
try {
Thread.sleep(200);//模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->拿到了第"+ticketNums--+"张票");
}
}
}
龟兔赛跑
/*
1.首先来个赛道距离,然后要离终点越来越近
2.判断比赛是否结束
3.打印出胜利者
4.龟兔赛跑开始
5.故事中是乌龟赢的,兔子需要睡觉,所以我们来模拟兔子睡觉
6.终于,乌龟赢得比赛
**/
public class Race implements Runnable{
private static String winner;//用static,保证只有一个胜利者
public static void main(String[] args) {
Race Rubbit=new Race();
Race tortise=new Race();
new Thread(Rubbit,"兔子").start();
new Thread(tortise,"乌龟").start();
}
@Override
public void run() {
for (int i = 0; i <= 1000; i++) {
//模拟兔子休息,每10步休息一下
if(Thread.currentThread().getName().equals("兔子")&&i%10==0)//注意不要用==
{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//判断比赛是否结束
boolean flag=gameOver(i);
//如果比赛结束,停止程序
if(flag)
{
break;
}
System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");
}
}
//判断是否完成比赛
private boolean gameOver(int steps)
{
if(winner!=null)
{
return true;
}
{
if(steps>=1000) {
winner= Thread.currentThread().getName();
System.out.println("winneer is"+winner);
return true;
}
}
return false;
}
}
线程状态
五大状态
创建状态:new
就绪状态:start
阻塞状态:sleep(sleep只是其中一个)
死亡状态:正常执行完
停止线程
不推荐使用jdk提供的stop(),destory()方法,建议使用一个标志位进行终止变量,当flag=false,则线程终止运行
public class Teststop implements Runnable {
//1.线程中定义线程体使用的标识
private boolean flag = true;
@Override
public void run (){
/ /2 .线程体使用该标识
while (flag) {
systepaoit.println ( "run. . . Thread" );
}
}
//3.对外提供方法改变标识
public void stop(){
this.flag = false;
}
}
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("run线程停止了!");
}
}
}
}
线程休眠(sleep)
sleep(时间)指定当前线程阻塞的毫秒数;
sleep存在异常InterruptedException;
sleep时间达到后线程进入就绪状态;
sleep可以模拟网络延时(放大问题的发生性,比如多线程卖票,一票多卖),倒计时等。
每一个对象都有一个锁,sleep不会释放锁。
package MultiThread;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.SimpleFormatter;
//模拟倒计时
public class TestSleep implements Runnable {
public static void main(String[] args) throws InterruptedException {
tenDown();
//打印当前系统时间
Date startTime=new Date(System.currentTimeMillis());
while(true)
{
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime=new Date(System.currentTimeMillis());
}
}
public static void tenDown() throws InterruptedException {//模拟倒计时
int num=10;
while(true)
{
Thread.sleep(1000);
System.out.println(num--);
if(num<=0)
{
break;
}
}
}
@Override
public void run() {
}
}
线程礼让(yield)
礼让线程,让当前正在执行的线程暂停,但不阻塞
将线程从运行状态转为就绪状态
让CPU重新调度,礼让不一定成功
package MultiThread;
public class TestYield {
public static void main(String[] args) {
MyYield myYield=new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程结束执行");
}
}
线程同步
多个线程操作同一个对象的安全问题(买火车票)
现实生活中,我们会遇到”同一个资源,多个人都想使用”的问题,比如,食堂排队打饭﹐每个人都想吃饭﹐最天然的解决办法就是﹐排队﹒一个个来.
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象.这时候我们就需要线程同步,线程同步其实就是一种等待机制。多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用
线程同步
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
一个线程持有锁会导致其它所有需要此锁的线程挂起;
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
线程同步形成条件:队列+锁
三大不安全案例
①买火车票
线程不安全
package MultiThread.syn;
//不安全的买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"1").start();
new Thread(buyTicket,"2").start();
new Thread(buyTicket,"3").start();
}
}
class BuyTicket implements Runnable{
private int tickNums=10;
boolean flag=true;//标志位,用于线程的外部停止方式
@Override
public void run() {
//买票
while(flag)
{
buy();
}
}
private void buy() {
//判断是否有票
if (tickNums <= 0)
{
flag=false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//买票
System.out.println(Thread.currentThread().getName()+"拿到"+tickNums--);
}
}
线程安全
//synchronized同步方法,锁的是this
private synchronized void buy() throws InterruptedException {
//判断是否有票
if (tickNums <= 0)
{
flag=false;
return;
}
//模拟延时
// Thread.sleep(10);
//买票
System.out.println(Thread.currentThread().getName()+"拿到"+tickNums--);
}
② 银行取钱
package MultiThread.syn;
//不安全取钱
public class UnsafeBank {
public static void main(String[] args) {
//取钱首先得有账户
Account account=new Account(100,"结婚基金");
Drawing you=new Drawing(account,50,"你");
Drawing girl=new Drawing(account,100,"girlFriend");
you.start();
girl.start();
}
}
class Account{//账户
int money;//余额
String name;//卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread{
Account account;//账户
int drawingMoney;//取多少钱
int nMoney;//现在有多少钱
public Drawing(Account account, int drawingMoney, String name) {
super(name);//调用父类的方法
this.account = account;
this.drawingMoney = drawingMoney;
// this.nMoney = nMoney;
}
@Override
public void run() {
//super.run();
//判断有没有钱
if(account.money-drawingMoney<0)
{
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额
account.money=account.money-drawingMoney;
//现在手里的钱
nMoney=nMoney+drawingMoney;
System.out.println(account.name+"余额为:"+account.money);
//this就是调用当前方法的对象,Drawing继承了Thread,所以this也是一个线程对象,可以调用Thread的getName方法来获取线程的名字
//Thread.currentThread().getName()=this.getName()
System.out.println(this.getName()+"手里的钱"+nMoney);
}
}
线程安全
@Override
public void run() {
//super.run();
//判断有没有钱
synchronized(account)
{
if(account.money-drawingMoney<0)
{
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额
account.money=account.money-drawingMoney;
//现在手里的钱
nMoney=nMoney+drawingMoney;
System.out.println(account.name+"余额为:"+account.money);
//this就是调用当前方法的对象,Drawing继承了Thread,所以this也是一个线程对象,可以调用Thread的getName方法来获取线程的名字
//Thread.currentThread().getName()=this.getName()
System.out.println(this.getName()+"手里的钱"+nMoney);
}
}
锁的对象就是多个线程共享的对象,如果每个线程的锁对象都不一样synchronized就没有用,锁谁取决于共同操作的对象是谁(且这个对象是要改变的)
③线程不安全的集合
import java.util.ArrayList;
import java.util.List;
//线程不安全的集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(100);//休息一会再打印,电脑运行速度太快了。线程里add还没执行完就执行主线程的打印语句。会导致打印结果偏小,但最后实际结果其实还是10000
System.out.println(list.size());
}
}
同步方法
由于我们可以通过private关键字来保证数据对象只能被方法访问﹐所以我们只需要针对方法提出一套机制。这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块.
同步方法:public synchronized void method(int args)f
synchronized方法控制对“对象”的访问,每个对象对应一把锁﹐每个synchronized方法都必须获得调用该方法的对象的锁才能执行﹐否则线程会阻塞,方法一旦执行﹐就独占该锁,直到该方法返回才释放锁﹐后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronized将会影响效率
只读代码是不需要加锁的,只有需要进行修改的代码才需要加锁(所以产生了同步方法和同步代码块)
方法里面需要修改的内容才需要锁,锁得太多,浪费资源
同步块
synchronized(Obj){}
Obj称之为同步监视器
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
注意监视的对象是需要增删改查的对象
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
同步监视器的执行过程:
第一个线程访问,锁定同步监视器,执行其中代码
第二个线程访问,发现同步监视器被锁定,无法访问
第一个线程访问完毕,皆出同步监视器
第二个线程访问,发现同步监视器没有锁
线程安全的集合:CopyOnWriteArrayList
我没有用lambda表达式创建线程,而是实现Runnable接口,结果证明这是可以的,且线程名字也存进了list
package MultiThread.syn;
import java.util.concurrent.CopyOnWriteArrayList;
public class testJuuc implements Runnable {
static CopyOnWriteArrayList<String>list=new CopyOnWriteArrayList<String>();
public static void main(String[] args) throws InterruptedException {
testJuuc thread=new testJuuc();
for (int i = 0; i < 10; i++) {
new Thread( thread).start();
}
Thread.sleep(3000);
System.out.println(list.size());
System.out.println(list.toString());
}
@Override
public void run() {
list.add(Thread.currentThread().getName());
}
}