@[toc]javascript
因为并发确定涉及到多线程,所以在进入并发编程主题以前,咱们先来了解一下进程和线程的由来,这对后面对并发编程的理解将会有很大的帮助。java
进程和线程的对比这一知识点因为过于基础,正由于过于基础,因此咱们更应该透彻它!咱们必须掌握什么是线程和进程,掌握线程与进程的关系、区别及优缺点 !程序员
首先咱们来看一下进程的概念:面试
进程:是指一个内存中运行的应用程序,每一个进程都有一个独立的内存空间,一个应用程序能够同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序便是一个进程从建立、运行到消亡的过程。数据库
看完以后,是否是感受很抽象?很懵bi?懵bi就对了,说明你和我智商同样高....~开个玩笑~编程
不妨先憋弃上面的概念,放松一下大脑,双击打开LOL,秒选德马打野,输了直接退出游戏而且保持微笑,而后正襟危坐心平气和的看宜春写的博客....安全
这个时候的你不只仅是愉快的撸了一把游戏,并且还亲自体验撸了一把进程...其实在你双击打开LOL的时候就已经建立了进程,此话怎讲?众所周知,咱们的电脑安装的软件好比:LOL、微信、谷歌等等都是存储在咱们的硬盘上的,硬盘上的数据能够说是永久存储(ORM),当咱们双击LOL的时候,LOL程序执行就进入了内存中,全部的程序必须进入内存中才能执行,内存属于临时存储(RAM),而进入内存的程序均可以叫作是进程,把LOL程序退出的时候,LOL程序就会退出内存,进程也就随之销毁了!所以说各位撸了一把进程也不为过吧。服务器
啥?字太多了,看的不够明了,不如看图得劲....额。。。 微信
一样的,咱们先来看线程的概念多线程
线程是进程中的一个执行单位,负责当前进程中程序的执行。一个进程中至少有一个线程,也就是说一个进程能够有多个线程的,而多个线程的进程运用程序就叫作多线程程序
线程的概念稍微好理解不少,可是想更深层次的去理解光靠上面一段文字的概述是彻底不够的!
这不打LOL的过程当中,属实卡的一批,果真花高价998买的6手戴尔笔记本打LOL属实像极了爱情。这个时候不得不双击打开电脑安全管家进行杀毒,果真2500天没有进行过病毒查杀,我天。。。其实我相信不少人都用过电脑管家或者手机管家之类的安全软件,咱们都很清楚咱们开启病毒查杀以后通常要几分钟扫描查杀,这个时候咱们是可让它后台进行的,咱们不会等而是开启另外一个垃圾清理的功能,这个时候咱们也不会等而是再去启动电脑加速功能。等到 这些操做都完成以后果断退出电脑管家,继续LOL,果真高价998买的6手戴尔笔记本再怎么杀毒打LOL仍是照样的卡....
其实清楚线程必然涉及到CPU的相关概念了,将上面文字所描述的用图片归纳,大体为:
从上一节中,咱们也提到过多线程,因此理解起来应该不难。
多线程就是多个线程同时运行 或 交替运行。
单核CPU:交替运行。 多核CPU:同时运行。
其实,多线程程序并不能提升程序的运行速度,但可以提升程序运行效率,让CPU的使用率更高。
提及线程调度优先级这个概念,就让我想到如今咱们大部分人投简历同样。若是你的学历或者工做经验越高,那么你的优先级就越高,面试官很大概率就会让你去面试但也不是必定只是概率特别大,若是线程的优先级相同,那么会随机选择一个(线程随机性)!在咱们每一个人的电脑中线程是能够设置线程的优先级的,可是生活中没有优先级(学历、工做经验)的孩子就只能靠本身的能力了~妈耶,太真实了...~
线程优先级具备继承特性好比A线程启动B线程,则B线程的优先级和A是同样的。
线程优先级具备随机性也就是说线程优先级高的不必定每一次都先执行完,只是被执行的可能性更大。
在从此的多线程学习旅游中咱们会使用到getPriority()
方法获取线程的优先级。
线程与进程类似,但线程是一个比进程更小的执行单位,是程序执行的最小单位。一个进程在其执行的过程当中能够产生多个线程。与进程不一样的是同类的多个线程共享同一块内存空间和一组系统资源,因此系统在产生一个线程,或是在各个线程之间做切换工做时,负担要比进程小得多,也正由于如此,线程也被称为轻量级进程。同时线程是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是由于线程间的切换和调度的成本远远小于进程。
而使用多线程,多线程会将程序运行方式从串行运行变为并发运行,效率会有很大提升。
在博主认为并发和并行是两个很是容易被混淆的概念。为了防止绕晕你们,因此我选择长话短说!
时间段
内同时发生(并非同时发生)。时刻
发生(真正的同时发生)。它们均可以表示两个或者多个任务一块儿执行,可是偏重点有些不一样。
于此同时,咱们不妨回顾一下上面所提到过的CPU,并再次理解并发与并行的区别,从而温故知新 ~我TM简直是个天才!~
单核CPU:交替运行【并发】 多核CPU:同时运行【并行】
并发给人的感受是同时运行,那是由于分时交替运行的时间是很是短的!
咱们常说的主线程就是Main线程,它是一个特殊的单线程,话很少说,直接撸码:
定义一个用于测试的demo类Person
package demo;
public class Person {
public String name;
public Person(String name){
this.name=name;
}
public void run(){
int i=1;
while (i<5){
System.out.println(name+i);
i++;
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
复制代码
编写Main方法
package demo;
public class MainThreadDemo {
public static void main(String[] args) {
Person per=new Person("常威");
per.run();
Person Per2=new Person("来福");
Per2.run();
}
}
复制代码
运行结果就已经很显而易见了,放心我不是靠大家运行结果而是单纯的先分析主线程。
运行结果:
常威1
常威2
常威3
常威4
来福1
来福2
来福3
来福4
复制代码
单线程不只效率低下,并且存在很大的局限性,唯一的优势就是安全。因此说女孩子长得安全其实也是一种优势,噗哈哈哈...
如何体现出单线程效率低下以及它的局限性呢?其实只要一句代码便可,仍是以上面的单线程Main线程为例:
package demo;
public class MainThreadDemo {
public static void main(String[] args) {
Person per=new Person("常威");
per.run();
int a=6/0; //=====================特别注意这行代码
Person Per2=new Person("来福");
Per2.run();
}
}
复制代码
试想一下运行结果...
言归正传,效率低下何以见得?这是数据少,若是是一亿条数据呢,单线程就是一个一个打印。那局限性又何以见得呢?从上面运行结果来看也能看出,只由于一行代码而致使下面代码再也不执行。已经很明显了。
说是说建立多线程有四种方式,但考虑到是入门文章仍是主要写入门的两种方式,剩下的两个暂时忽略。忽略的两种方法有:实现Callable
接口经过FutureTask
包装器来建立Thread线程、使用ExecutorService
、Callable
、Future
实现有返回结果的线程。如今可能对于入门的童鞋来讲是接收不了的,之后再去了解也不晚!
Java使用java.lang.Thread
类表明线程,全部的线程对象都必须是Thread类或其子类的实例。每一个线程的做用是完成必定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来表明这段程序流。
Java中经过继承Thread
类来建立并启动多线程的步骤以下:
Thread
类的子类,并重写该类的run()
方法,该run()
方法的方法体就表明了线程须要完成的任务,所以把run()
方法称为线程执行体。Thread
子类的实例,即建立了线程对象start()
方法来启动该线程代码以下:
测试类:
public class Demo01 {
public static void main(String[] args) {
//建立自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
复制代码
自定义线程类:
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/** * 重写run方法,完成该线程执行的逻辑 */
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
复制代码
Thread
类本质上是实现了Runnable
接口的一个实例,表明一个线程的实例。启动线程的惟一方法就是经过Thread
类的start()
实例方法。start()
方法是一个native
方法,它将启动一个新线程,并执行run()
方法。这种方式实现多线程很简单,经过本身的类直接extend Thread
,并复写run()
方法,就能够启动新线程并执行本身定义的run()
方法。
若是本身的类已经继承另外一个类,就没法直接继承Thread
,此时,能够实现一个Runnable
接口来建立线程,显然实现Runnable
接口方式建立线程的优点就很明显了。
直接撸码:
自定义一个类实现Runnable接口,并重写接口中的run()方法,并为run方法添加要执行的代码方法。
public class RunableDemo implements Runnable{
@Override
public void run() {
int a = 1;
while (a<20){
System.out.println(Thread.currentThread().getName()+ a);//Thread.currentThread().getName()为获取当前线程的名字
a++;
}
}
}
复制代码
编写Main方法
为了启动自定义类RunableDemo
,须要首先实例化一个Thread,并传入RunableDemo 实例:
public class MainThreadDemo {
public static void main(String[] args) {
RunableDemo runn=new RunableDemo();
//实例化一个Thread并传入本身的RunableDemo 实例
Thread thread=new Thread(runn);
thread.start();
int a = 1;
while (a<20){
//Thread.currentThread().getName()为获取当前线程的名字
System.out.println(Thread.currentThread().getName()+ a);
a++;
}
}
}
复制代码
运行结果:
main1
main2
main3
Thread-01
Thread-02
Thread-03
Thread-04
Thread-05
Thread-06
....
复制代码
其实多运行几遍,你会方法每次运行的结果顺序都不同,这主要是因为多线程会去抢占CPU的资源,谁抢到了谁就执行,而Main和Thread两个线程一直在争抢。
实际上,当传入一个Runnable target
(目标)参数给Thread
后,Thread
的run()
方法就会调用target.run()
,参考JDK源代码:
public void run() {
  if (target != null) {
   target.run();
  }
}
复制代码
采用继承Thread类方式:
(1)优势:编写简单,若是须要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,便可得到当前线程。 (2)缺点:由于线程类已经继承了Thread类,因此不能再继承其余的父类。
采用实现Runnable接口方式:
(1)优势:线程类只是实现了Runable接口,还能够继承其余的类。在这种方式下,能够多个线程共享同一个目标对象,因此很是适合多个相 同线程来处理同一份资源的状况,从而能够将CPU代码和数据分开,造成清晰的模型,较好地体现了面向对象的思想。 (2)缺点:编程稍微复杂,若是须要访问当前线程,必须使用Thread.currentThread()方法。
小结: 若是一个类继承Thread,则不适合资源共享。可是若是实现了Runable
接口的话,则很容易的实现资源共享。
实现Runnable
接口比继承Thread
类的优点:
1.适合多个相同代码的线程去处理同一个资源。
2.能够避免java中单继承的限制。
3.增长代码的健壮性,实现解耦。代码能够被多个线程共享,代码和数据独立。
4.线程池中只能放入实现Runnable或Callable类线程,不能放入继承Thread的类【线程池概念以后会慢慢涉及】
因此,若是选择哪一种方式,尽可能选择实现Runnable
接口!
其实学到后面的线程池,你会发现上面两种建立线程的方法实际上不多使用,通常都是用线程池的方式比较多一点。使用线程池的方式也是最推荐的一种方式,另外,《阿里巴巴Java开发手册》在第一章第六节并发处理这一部分也强调到“线程资源必须经过线程池提供,不容许在应用中自行显示建立线程”。不过处于入门阶段的童鞋博主仍是强烈建议一步一个脚印比较好!
谈起匿名内部类,可能不少小白是比较陌生的,毕竟开发中使用的仍是比较少,可是一样是很是重要的一个知识!于此同时我就贴出关于匿名内部类的文章程序员你真的理解匿名内部类吗?若是小白童鞋能看懂下面这个代码,真的你不须要看那篇文章了,你T喵的简直是个天才!
package AnonymousInner;
public class NiMingInnerClassThread {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i<5;i++){
System.out.println("熊孩子:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 5 ; i++){
System.out.println("傻狍子:"+i);
}
}
}
复制代码
小白童鞋还愣着干啥呀赶忙去补补...
线程安全问题主要是共享资源竞争的问题,也就是在多个线程状况下,一个或多个线程同时抢占同一资源致使出现的一些没必要要的问题,最典型的例子就是火车四个窗口售票问题了,这里就再也不举售票例子了,已经烂大街了,这里就简单实现一个线程安全问题代码....
实现Runnable接口方式为例,主要实现过程是:实例化三个Thread,并传入同一个RunableDemo 实例做为参数,最后开启三条相同参数的线程,代码以下:
public class RunableDemo implements Runnable{
public int a = 100;//线程共享数据
@Override
public void run() {
while (a>0){
System.out.println("线程"+Thread.currentThread().getName()+"执行到"+ a);
a--;
}
}
}
复制代码
public class MainThreadDemo {
public static void main(String[] args) {
RunableDemo runn=new RunableDemo();
Thread thread1=new Thread(runn);
Thread thread2=new Thread(runn);
Thread thread3=new Thread(runn);
thread1.start();
thread2.start();
thread3.start();
}
}
复制代码
运行结果:
Thread-0==100
Thread-0==99
Thread-1==100
Thread-1==97
Thread-1==96
Thread-1==95
Thread-2==98
...
复制代码
根据结果能够看出,确实是三条线程(Thread-0、一、2)在执行,安全问题就出在线程会出现相同的结果好比上面的100就出现了两次,若是循环条件更改一下可能也会出现负数的状况。这种状况该怎么解决呢?这个时候就须要线程同步了!
实际上,线程安全问题的解决方法有三种:
一、同步代码块 二、同步方法 三、锁机制
第一种方法:同步代码块
格式:
synchronized(锁对象) {
可能会出现线程安全问题的代码(访问共享数据的代码)
}
复制代码
使用同步代码块特别注意: 一、经过代码块的锁对象,能够是任意对象 二、必须保证多个线程使用的锁对象必须是同一个 三、锁对象的做用是把同步代码快锁住,只容许一个线程在同步代码块执行
仍是以上面线程安全问题为例子,使用同步代码块举例:
public class RunableDemo implements Runnable{
public int a = 100;//线程共享数据
Object object=new Object(); //事先准备好一个锁对象
@Override
public void run() {
synchronized (object){ //使用同步代码块
while (a>0){
System.out.println("线程"+Thread.currentThread().getName()+"执行到"+ a);
a--;
}
}
}
}
复制代码
Main方法没有任何改动,运行一下结果是绝对没问题的,数据都是正确的没有出现重复状况这一出,各位能够本身尝试一下!
同步代码块的原理:
使用了一个锁对象,叫同步锁,对象锁,也叫同步监视器,当开启多个线程的时候,多个线程就开始抢夺CPU的执行权,好比如今t0线程首先的到执行,就会开始执行run方法,遇到同步代码快,首先检查是否有锁对象,发现有,则获取该锁对象,执行同步代码块中的代码。以后当CUP切换线程时,好比t1获得执行,也开始执行run方法,可是遇到同步代码块检查是否有锁对象时发现没有锁对象,t1便被阻塞,等待t0执行完毕同步代码块,释放锁对象,t1才能够获取从而进入同步代码块执行。 同步中的线程,没有执行完毕是不会释放锁的,这样便实现了线程对临界区的互斥访问,保证了共享数据安全。 缺点:频繁的获取释放锁对象,下降程序效率
使用步骤:
一、把访问了共享数据的代码抽取出来,放到一个方法中 二、在该方法上添加
synchronized
修饰符
格式:
修饰符 synchronized 返回值类型 方法名称(参数列表) {
方法体...
}
复制代码
代码示例:
public class RunableDemo implements Runnable{
public int a = 100;//线程共享数据
@Override
public void run() {
while (true){
sell(); //调用下面的sell方法
}
}
//访问了共享数据的代码抽取出来,放到一个方法sell中
public synchronized void sell(){
while (a>0){
System.out.println("线程"+Thread.currentThread().getName()+"执行到"+ a);
a--;
}
}
}
复制代码
同步方法的也是同样锁住同步的代码,可是锁对象的是Runable
实现类对象,也就是this
,谁调用方法,就是谁。
说到同步方法,就不得不说一下静态同步方法,顾名思义,就是在同步方法上加上static,静态的同步方法,添加一个静态static
修饰符,此时锁对象就不是this
了,静态同步方法的锁对象是本类的class属性,class文件对象(反射)
public class RunableDemo implements Runnable{
public static int a = 100;//线程共享数据 =====此时共享数据也要加上static
@Override
public void run() {
while (true){
sell();
}
}
public static synchronized void sell(){ //注意添加了static关键字
while (a>0){
System.out.println("线程"+Thread.currentThread().getName()+"执行到"+ a);
a--;
}
}
}
复制代码
使用静态同步方法时,此时共享数据也要加上static,由于static成员才能访问static成员,若是对static关键字不是他别理解的能够补补了,放心,博主有信心让你有所收获,会让你从新认识到static的魅力:深刻理解static关键字
固然静态同步方法了解便可!
Lock接口位于java.util.concurrent.locks.Lock
它是JDK1.5以后出现的,Lock接口中的方法:
void lock()
: 获取锁
void unlock()
: 释放锁
Lock接口的一个实现类java.util.concurrent.locks.ReentrantLock implements Lock
接口
使用方法: 一、在Runable
实现类的成员变量建立一个ReentrantLock
对象 二、在可能产生线程安全问题的代码前该对象调用lock
方法获取锁 三、在可能产生线程安全问题的代码后该对象调用unlock
方法释放锁
代码示例:
import java.util.concurrent.locks.ReentrantLock;
public class RunableDemo implements Runnable{
public static int a = 100;//线程共享数据
//一、在Runable实现类的成员变量建立一个ReentrantLock对象============
ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
// 二、在可能产生线程安全问题的代码前该对象调用lock方法获取锁=======
reentrantLock.lock();
while (a>0){
System.out.println("线程"+Thread.currentThread().getName()+"执行到"+ a);
a--;
}
// 三、在可能产生线程安全问题的代码后该对象调用unlock方法获取锁======
reentrantLock.unlock();
}
}
复制代码
固然更安全的写法是,在线程安全问题代码中try...catchy
,最后在finally
语句中添加reentrantLock.unlock();
,这样方为上上策!
第一种 synchronized 同步代码块:能够是任意的对象必须保证多个线程使用的锁对象是同一个
第二种 synchronized 同步方法: 锁对象是this,谁调用锁对象就是谁
synchronized 静态同步方法: 锁对象是其class对象,该对象能够用
this.getClass()
方法获取,也可使用当前类名.class
表示。【了解便可】
第三种 Look锁方法:该方法提供的方法远远多于
synchronized
方式,主要在Runable实现类的成员变量建立一个ReentrantLock
对象,并使用该对象调用lock
方法获取锁以及unlock
方法释放锁!
Thread()
:用于构造一个新的Thread。
Thread(Runnable target)
:用于构造一个新的Thread,该线程使用了指定target的run方法。
Thread(ThreadGroup group,Runnable target)
:用于在指定的线程组中构造一个新的Thread,该
线程使用了指定target的run方法。
currentThread()
:得到当前运行线程的对象引用。
interrupt()
:将当前线程置为中断状态。
sleep(long millis)
:使当前运行的线程进入睡眠状态,睡眠时间至少为指定毫秒数。
join()
:等待这个线程结束,即在一个线程中调用other.join(),将等待other线程结束后才继续本线程。
yield()
:当前执行的线程让出CPU的使用权,从运行状态进入就绪状态,让其余就绪线程执行。
wait()
:让当前线程进入等待阻塞状态,直到其余线程调用了此对象的notify()或notifyAll()方法后,当前线程才被唤醒进入就绪状态。
notify()
:唤醒在此对象监控器(锁对象)上等待的单个线程。
notifyAll()
:唤醒在此对象监控器(锁对象)上等待的全部线程。
注意:
wait()、notify()、notifyAll()
都依赖于同步锁,而同步锁是对象持有的,且每一个对象只有一个,因此这些方法定义在Object
类中,而不是Thread
类中。
wait()
:让线程从运行状态进入等待阻塞状态,而且会释放它所持有的同步锁。
yield()
:让线程从运行状态进入就绪状态,不会释放它锁持有的同步锁。
sleep()
:让线程从运行状态进入阻塞状态,不会释放它锁持有的同步锁。
若是想要去深刻了解一下的话也是能够的:Java线程的6种状态及切换
在java中只要说到池,基本都是一个套路,啥数据库链接池、jdbc链接池等,思想基本上就是:一个容纳多个要使用资源的容器,其中的资源能够反复使用,省去了频繁建立线程对象的操做,无需反复建立资源而消耗过多资源。
线程池其实就是一个容纳多个线程的容器,其中的线程能够反复使用,省去了频繁建立线程对象的操做,无需反复建立线程而消耗过多资源。
合理利用线程池可以带来三个好处:
- 下降资源消耗。减小了建立和销毁线程的次数,每一个工做线程均可以被重复利用,可执行多个任务。
- 提升响应速度。当任务到达时,任务能够不须要的等到线程建立就能当即执行。
- 提升线程的可管理性。能够根据系统的承受能力,调整线程池中工做线线程的数目,防止由于消耗过多的内存,而把服务器累趴下(每一个线程须要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
Java里面线程池的最顶级接口是java.util.concurrent.Executor
,可是严格意义上讲Executor
并非一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,尤为是对于线程池的原理不是很清楚的状况下,颇有可能配置的线程池不是较优的,所以在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些经常使用的线程池。官方建议使用Executors
工程类来建立线程池对象。
Executors类中有个建立线程池的方法以下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(建立的是有界线程池,也就是池中的线程个数能够指定最大数量)获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法以下:
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行Future接口:用来记录线程任务执行完毕后产生的结果。线程池建立与使用。
使用线程池中线程对象的步骤:
- 建立线程池对象。
- 建立Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(通常不操做这一步)。
Runnable实现类代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个游泳教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,教会后,教练又回到了游泳池");
}
}
复制代码
线程池测试类:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 建立线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 建立Runnable实例对象
MyRunnable r = new MyRunnable();
//本身建立线程对象的方式
// Thread t = new Thread(r);
// t.start(); ---> 调用MyRunnable中的run()
// 从线程池中获取线程对象,而后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是由于线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
//service.shutdown();
}
}
复制代码
以上只是简单的使用线程池,仅仅是入门阶段!道阻且长,路还很长....
到这里,本文章入门暂时告一段落,之后有时间尽可能抽空更新....
若是本文章对你有帮助,哪怕是一点点,请点个赞呗,谢谢你~
欢迎各位关注个人公众号,一块儿探讨技术,向往技术,追求技术...说好了来了就是盆友喔...