新建线程
-
继承
Thread
类,重写run
方法。创建线程对象,调用start()
方法。不同线程由CPU调度
下面是一个多线程下载图片的示例程序。其中使用了commons.io 包。
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
public class Test extends Thread{
private String url;
private String name;
public Test(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
Downloader.download(url, name);
}
public static void main(String[] args) {
Test test = new Test("https://image5.cnpp.cn/upload/images/20190531/13513712154_1200x800.jpg", "故宫.jpg");
Test test1 = new Test("https://image5.cnpp.cn/upload/images/20190531/13513712154_1200x800.jpg", "故宫1.jpg");
Test test2 = new Test("https://image5.cnpp.cn/upload/images/20190531/13513712154_1200x800.jpg", "故宫2.jpg");
test.start();
test1.start();
test2.start();
}
}
class Downloader{
public static void download(String url,String file){
try{
FileUtils.copyURLToFile(new URL(url), new File(file));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println(file + " 已经下载完成");
}
}
-
实现
Runnable
接口实现接口,重写run方法,将对象传入Thread构造器,调用start方法。
实现Runnable接口实现多线程下载网图:
import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; public class Test implements Runnable{ private String url; private String name; public Test(String url, String name) { this.url = url; this.name = name; } @Override public void run() { Downloader.download(url, name); } public static void main(String[] args) { Test test = new Test("https://image5.cnpp.cn/upload/images/20190531/13513712154_1200x800.jpg", "故宫.jpg"); Test test1 = new Test("https://image5.cnpp.cn/upload/images/20190531/13513712154_1200x800.jpg", "故宫1.jpg"); Test test2 = new Test("https://image5.cnpp.cn/upload/images/20190531/13513712154_1200x800.jpg", "故宫2.jpg"); new Thread(test).start(); new Thread(test1).start(); new Thread(test2).start(); } } class Downloader{ public static void download(String url,String file){ try{ FileUtils.copyURLToFile(new URL(url), new File(file)); } catch (MalformedURLException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } System.out.println(file + " 已经下载完成"); } }
-
同步问题
public class Test implements Runnable { private int num = 1; @Override public void run() { while (num<=10){ System.out.println(Thread.currentThread().getName() + "获得了第" + num++ + "张票"); try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static void main(String[] args) { Test test = new Test(); new Thread(test, "P1").start(); new Thread(test, "P2").start(); new Thread(test, "P3").start(); } }
上面的代码,对共享资源的访问出现了问题。(可能同时获得了相同的num,逻辑上有问题,可能造成无法预知的后果)
public class Test implements Runnable{ @Override public void run() { for (int i = 0; i <=100; i++) { System.out.println(i); } } public static void main(String[] args) { Test test = new Test(); new Thread(test, "P1").start(); new Thread(test, "P2").start(); } }
上面的代码,P1、P2都会拥有自己独立的for循环以及变量i,两个线程修改 i 的操作是独立的,但是修改自己的 i 的机会是竞争的。
public class Test implements Runnable{ int i = 10; @Override public void run() { while (i >= 0) { System.out.println(Thread.currentThread().getName() + "获得了 " + i--); try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static void main(String[] args) { Test test = new Test(); new Thread(test, "P1").start(); new Thread(test, "P2").start(); } }
在这段代码中,两个线程
P1
和P2
共享了同一个Test
实例,并且都会执行run()
方法。在run()
方法中,i
是一个实例变量,即对象的属性,而不是局部变量。因此,i
在这里是一个共享变量。由于两个线程共享了同一个
Test
实例,并且都在循环中对i
进行修改,会导致竞争条件(Race Condition)的问题。竞争条件是指多个线程对共享变量的访问和修改没有进行正确的同步,从而导致结果不可预测。在这个例子中,两个线程都在循环中对
i
进行自减操作,如果没有适当的同步措施,可能会导致以下问题:- 竞态条件:两个线程可能同时读取
i
的值,并且在对其进行自减操作时产生竞争,导致相同的值被多次打印。 - 不正确的输出:由于两个线程同时修改
i
,可能导致打印的结果不符合预期,例如出现负数或跳过某些值。
要解决这个问题,可以使用同步机制,例如使用
synchronized
关键字或Lock
来确保在任意时刻只有一个线程能够修改i
,从而避免竞态条件。 - 竞态条件:两个线程可能同时读取
-
通过实现Callable接口实现多线程
步骤:
- 实现Callable接口(设置返回值)
- 重写call方法,设置返回值
- 创建对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(100);(100为线程的数量)
- 提交执行:Future r1 = ser.submit(t1);
- 获取结果:boolean bs1 = r1.get();
- 关闭服务:ser.shutdownNow();
此方法的优点:
通过实现
Callable
接口实现多线程相比实现Runnable
接口有以下优点:-
返回结果:
Callable
接口的call()
方法可以有返回值,而Runnable
接口的run()
方法没有返回值。这使得在使用Callable
时可以方便地获取并处理线程的执行结果。 -
抛出异常:
Callable
的call()
方法可以抛出受检查的异常,而Runnable
的run()
方法只能抛出未受检查的RuntimeException
。这使得在使用Callable
时可以更好地处理异常情况。 -
使用
Future
:通过ExecutorService
的submit()
方法执行Callable
任务后,可以得到一个Future
对象,用于获取异步执行结果,或者取消任务执行等操作。Future
提供了更多的控制和灵活性。 -
能力与结果解耦:使用
Callable
可以将任务的能力(Callable
接口的call()
方法)与任务的执行结果(返回值)解耦。这样,不同的任务可以共享相同的能力接口,并且每个任务的执行结果可以有所不同。
总之,使用
Callable
接口实现多线程可以在多线程编程中提供更多的功能和控制选项,特别是在需要获取返回结果、处理异常和对任务执行进行更精细控制时,Callable
接口是一个更好的选择。 -
静态代理
Java的静态代理是一种设计模式,它通过创建一个代理类来代替原始类的直接访问,从而实现对原始类的访问控制。静态代理在编译期间就已经确定了代理类和被代理类的关系,因此称为静态代理。
静态代理涉及三个角色:
- 接口(或抽象类):定义了代理类和被代理类之间的共同行为,它是代理类和被代理类的约束接口。
- 被代理类:实际进行业务处理的原始类,它实现了接口(或继承了抽象类)。
- 代理类:代理被代理类,实现了相同的接口(或继承了相同的抽象类),并在其中持有被代理类的实例。代理类在调用接口方法时,会在内部调用被代理类的相应方法,并可以在调用前后添加额外的逻辑,实现对原始类的增强。
静态代理的优点包括:
- 可以在不修改原始类代码的情况下,通过代理类添加额外的功能或控制访问权限。
- 对原始类的调用可以在代理类中进行控制和管理,更加灵活和可控。
- 代理类和原始类分离,符合单一职责原则,便于维护和扩展。
以下是一个简单的Java静态代理的示例代码:
javaCopy code// 定义接口 interface Subject { void doSomething(); } // 被代理类 class RealSubject implements Subject { @Override public void doSomething() { System.out.println("RealSubject is doing something."); } } // 代理类 class ProxySubject implements Subject { private RealSubject realSubject; public ProxySubject(RealSubject realSubject) { this.realSubject = realSubject; } @Override public void doSomething() { System.out.println("Before doing something."); realSubject.doSomething(); // 调用被代理类的方法 System.out.println("After doing something."); } } public class StaticProxyExample { public static void main(String[] args) { RealSubject realSubject = new RealSubject(); // 创建被代理类的实例 ProxySubject proxy = new ProxySubject(realSubject); // 创建代理类的实例 proxy.doSomething(); // 调用代理类的方法,实际会调用被代理类的方法 } }
在上述代码中,
Subject
是接口,RealSubject
是被代理类,ProxySubject
是代理类。代理类持有一个被代理类的实例,并在其doSomething()
方法中调用被代理类的方法,同时添加了额外的前后逻辑。通过调用代理类的方法,实际上会调用被代理类的方法,并且在执行前后会输出额外的信息。请注意,这只是一个简单的示例,实际使用中静态代理的应用场景可能更加复杂,但基本的原理和结构是类似的。
public class Test { public static void main(String[] args) { Person person = new Person(); Server server = new Server(person); server.say(); } } interface Speak{ void say(); } class Person implements Speak { @Override public void say() { System.out.println("我是人类"); } } class Server implements Speak { private Speak s; public Server(Speak s) { this.s = s; } @Override public void say() { System.out.println("之前"); s.say(); System.out.println("之后"); } }
上面的Server构造器中传入的是接口变量。任意实现了这个接口的类的对象,都可以交由Server这个代理类来操作。客户端只需要关注这个接口即可,而不需要关心每个被代理对象的具体操作。
这种设计方式非常灵活和可扩展,因为在将来如果需要增加新的被代理类,只需要让该类实现
Speak
接口,然后将其对象传递给Server
类的构造器即可。无需对Server
类的代码做任何修改,从而实现了代码的解耦和灵活性。同时,通过面向接口编程,我们也能更好地利用多态特性,提高代码的可维护性和扩展性。这种设计模式与创建线程非常相似。下面的新建的Thread对象就是代理类,lambda表达式就是具体的被代理类的对象。每个线程具体的操作可能不同,但是都被Thread类代理,用户只需要关注线程的具体操作即可。
public class NewThread { public static void main(String[] args) { new Thread(()->{ System.out.println("线程结束"); }).start(); } }
通过将lambda表达式作为具体的被代理类对象,我们可以在创建线程时轻松地定义线程的具体操作,而无需显式创建一个新的类来实现
Runnable
接口或继承Thread
类。在这个示例中,
Thread
类就是代理类,它将具体的线程操作委托给传递进来的lambda表达式(即被代理类对象),从而实现了线程的创建和执行。客户端只需要关注具体的线程操作,而不需要担心线程的管理和执行细节,这就是面向接口编程的优势之一。通过lambda表达式的简洁语法,我们可以快速创建轻量级的线程,并且可以在创建线程时直接定义线程要执行的任务,这样可以使代码更加简洁、清晰,并且提高了代码的可读性和可维护性。同时,这种方式也利用了Java 8引入的函数式编程特性,使代码更加灵活和表达力强。