Java中的多线程
目录
线程基础
线程是程序运行的基本执行单元。当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被称为主线程)作为这个程序运行的入口点。因此,在操作系统中运行的任何程序都至少有一个主线程。
一个进程中可以有一个或多个线程,进程和进程之间不共享内存,也就是说系统中的进程是在各自独立的内存空间中运行的。而一个进程中的线程可以共享系统分派给这个进程的内存空间。
线程不仅可以共享进程的内存,而且还拥有一个属于自己的内存空间,这段内存空间也叫做线程栈,是在建立线程时由系统分配的,主要用来保存线程内部所使用的数据,如线程执行函数中所定义的变量。
创建线程
使用Thread创建线程
Thread类的构造函数
public Thread();
public Thread(String name);
public Thread(Runnable target);
public Thread(ThreadGroup group, Runnable target);
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) ;
public Thread(ThreadGroup group, String name) ;
public Thread(Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name) ;
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize, boolean inheritThreadLocals) ;
参数说明:
- Runnable target:实现了Runnable接口的类的实例。
- String name:线程的名字,此名字可以在建立Thread实例后通过Thread类的setName方法设置。如果不设置线程的名字,线程就使用默认的线程名:Thread-N,N是线程建立的顺序,是一个不重复的正整数。
- ThreadGroup group:当前建立的线程所属的线程组。如果不指定线程组,所有的线程都被加到一个默认的线程组中。
- long stackSize:线程栈的大小。
通过Thread类的start()方法来执行线程代码
实例
public class MyThread extends Thread {
public void run(){
System.out.println(this.getName());
}
public static void main(String[] args) {
Thread th1=new MyThread();
Thread th2=new MyThread();
th1.start();//启动线程
th2.start();//启动线程
}
}
运行结果
Thread-0
Thread-1
使用Runnable接口创建线程
public class MyRunnable implements Runnable {
public void run(){
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
MyRunnable rb1=new MyRunnable();
MyRunnable rb2=new MyRunnable();
Thread th1=new Thread(rb1);
Thread th2=new Thread(rb2);
th1.start();
th2.start();
}
}
运行结果
Thread-0
Thread-1
控制线程
使用join()方法
Thread类的join()方法,此方法的功能是使异步执行的线程变成同步执行。而使用join()方法后,直到这个线程退出,程序才会往下执行。
加入join()方法后的代码实例
public class MyThread extends Thread {
public void run(){
try{
sleep(3000);
System.out.println(this.getName());
}catch(InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args){
Thread th1=new MyThread();
th1.start();
th1.join();
System.out.println("hello");
}
}
运行结果
Thread-0
hello
即使在线程中设置了3秒的等待,加入了join方法后,也要等线程运行完了才运行join后面的程序。
没有加入join方法的代码示例
public class MyThread extends Thread {
public void run(){
try{
sleep(3000);
System.out.println(this.getName());
}catch(InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread th1=new MyThread();
th1.start();
System.out.println("hello");
}
}
运行结果
hello
Thread-0
可以看出没有加入join()方法,线程和主线程异步进行。
总结:当需要线程中的返回值的时候,就可以加上join()方法,使得线程与主线程是同步进行的。
后台线程(守护线程)
有一种线程是在后台运行的,其任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程,后台线程有一个非常明显的特征——如果所有的前台线程都死亡,后台线程会自动死亡。
实例
setDaemon()
public class MyThread extends Thread {
public void run(){
for(int i=0;i<1000;i++){
System.out.println(this.getName()+" "+i);
}
}
public static void main(String[] args) {
Thread th1=new MyThread();
th1.setDaemon(true);//设置为后台进程
th1.start();
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}//main线程死亡
}
运行结果
main 0
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
main 1
Thread-0 9
Thread-0 10
main 2
Thread-0 11
main 3
Thread-0 12
main 4
Thread-0 13
Thread-0 14
Thread-0 15
Thread-0 16
Thread-0 17
Thread-0 18
Thread-0 19
Thread-0 20
Thread-0 21
Thread-0 22
Thread-0 23
Thread-0 24
Thread-0 25
Thread-0 26
Thread-0 27
Thread-0 28
Thread-0 29
可以看出新建的线程是没有运行完的,当主线程main运行结束后,Thread-0也结束了没有继续运行。
睡眠线程
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep方法实现,方法sleep有如下两种重载的形式。
//让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,
public static native void sleep(long millis);
//让当前正在执行的线程暂停millis毫秒加nanos毫微秒,并进入阻塞状态
public static void sleep(long millis, int nanos);
实例
public class MyThread extends Thread {
public void run(){
for(int i=0;i<5;i++){
System.out.println(this.getName()+" "+i);
}
}
public static void main(String[] args) throws InterruptedException{
Thread th1=new MyThread();
Thread th2=new MyThread();
Thread th3=new MyThread();
th1.start();
th2.start();
Thread.sleep(100);//线程睡眠100毫秒
th3.start();
}
}
运行结果
Thread-1 0
Thread-0 0
Thread-1 1
Thread-0 1
Thread-1 2
Thread-0 2
Thread-1 3
Thread-0 3
Thread-1 4
Thread-0 4
Thread-2 0
Thread-2 1
Thread-2 2
Thread-2 3
Thread-2 4
可以看出线程Thread-0和Thread-1正常运行,线程Thread-2会等待200毫秒在运行。
线程同步
可以使用join()方法
实例
public class Demo {
public static void main(String[] args) throws Exception{
Thread add=new AddThread();
Thread de=new DeThread();
add.start();
add.join();
de.start();
de.join();
System.out.println(Counter.count);
}
}
class Counter{
public static int count=0;
}
class AddThread extends Thread{
public void run(){
for(int i=0;i<10000;i++){
Counter.count+=1;
}
}
}
class DeThread extends Thread{
public void run(){
for(int i=0;i<10000;i++){
Counter.count-=1;
}
}
}
运行结果
0
假如没有使用join方法,最后的数据会是各种各样的,这样活导致数据不安全。
也可以使用synchronized进行加锁
为了一段代码的原子性,可以通过加锁和解锁的方式,格式如下:
synchronized(lock) {//加锁
...
}//解锁
实例
public class Demo {
public static void main(String[] args) throws Exception{
Thread add=new AddThread();
Thread de=new DeThread();
add.start();
de.start();
de.join();
System.out.println(Counter.count);
}
}
class Counter{
public static final Object lock = new Object();
public static int count=0;
}
class AddThread extends Thread{
public void run(){
for(int i=0;i<10000;i++){
synchronized(Counter.lock){
Counter.count+=1;
}
}
}
}
class DeThread extends Thread{
public void run(){
for(int i=0;i<10000;i++){
synchronized(Counter.lock){
Counter.count-=1;
}
}
}
}
运行结果
0
代码中加了de.join();只是为了让线程运行完了才打印,不然会出现其他的数字。
方法同步
为了增加线程的效率,可以使用synchronized对方法进行同步。
实例
public class Demo {
public static void main(String[] args) throws Exception{
Counter c1=new Counter();
Thread th1=new Thread(()->{c1.add();});
Thread th2=new Thread(()->{c1.dec();});
th1.start();
th2.start();
th2.join();
System.out.println(Counter.count);
}
}
class Counter{
public static int count=0;
public synchronized void add(){
for(int i=0;i<10000;i++){
Counter.count+=1;
}
}
public synchronized void dec(){
for(int i=0;i<10000;i++){
Counter.count-=1;
}
}
}
运行结果
0
这样即使多个线程去操作同一个数也是数据安全的,通过合理的设计和数据封装可以让一个类变为“线程安全”。
总结
线程和函数的关系
任何一个线程在建立时都会执行一个函数,这个函数叫做线程执行函数。也可以将这个函数看做线程的入口点(类似于程序中的main函数)。无论使用什么语言或技术来建立线程,都必须执行这个函数(这个函数的表现形式可能不一样,但都会有一个这样的函数)。
在run方法中使用线程名时带来的问题
在调用start()方法前后都可以使用setName设置线程名,但在调用start()方法后使用setName修改线程名,会产生不确定性,也就是说可能在run()方法执行完后才会执行setName。如果在run()方法中要使用线程名,就会出现虽然调用了setName()方法,但线程名却未修改的现象,类Thread的start()方法不能多次调用,否则会抛出异常。
继承Thread类或实现Runnable接口方式的比较
当采用Runnable接口方式实现多线程时,线程类只是实现了Runnable接口,还可以继承其他类。
当采用继承Thread类方式的多线程时,劣势是因为线程类已经继承了Thread类,所以不能再继承其他父类;
start()和run()的区别
使用方法start()来启动线程
方法run()只是thread的一个普通方法调用